From 914712fa45f0aecf4a357264709c81ce0ede2b8d Mon Sep 17 00:00:00 2001 From: Aadinath Sanjeev Jayanthi Date: Tue, 2 Dec 2025 14:26:14 -0500 Subject: [PATCH 01/24] feat: update MLH badge for 2026 season and change Open Graph URL to rockethacks.org --- src/app/layout.tsx | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 94fd5c1..23623d5 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -25,7 +25,7 @@ export const metadata: Metadata = { openGraph: { title: "RocketHacks 2026 | Hack the Horizon", description: "Join the biggest hackathon in the Midwest at the University of Toledo", - url: "https://rockethacks.github.io", + url: "https://rockethacks.org", siteName: "RocketHacks", images: [ { @@ -53,25 +53,23 @@ export default function RootLayout({ return ( - {/* Temporarily hidden MLH badge - uncomment to enable */} - {/* + {/* MLH 2026 Trust Badge - Positioned far right to avoid profile icon */} - Major League Hacking 2025 - */} {children} From 1aa38606d5f92588ce79c479c8d995118c8b3103 Mon Sep 17 00:00:00 2001 From: Aadinath Sanjeev Jayanthi Date: Thu, 4 Dec 2025 10:58:18 -0500 Subject: [PATCH 02/24] feat: add registration call-to-action for Code & Create 2026 and update links --- src/app/apply/page.tsx | 14 +- src/app/apply/page_new.tsx | 714 ----------------------------------- src/app/code-create/page.tsx | 43 ++- 3 files changed, 41 insertions(+), 730 deletions(-) delete mode 100644 src/app/apply/page_new.tsx diff --git a/src/app/apply/page.tsx b/src/app/apply/page.tsx index dd51e6b..de31c8f 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 4372f60..0000000 --- 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 ( -
-
- Background -
-
-
Loading...
-
- ) - } - - return ( -
- {/* Background */} -
- RocketHacks 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 d4107bc..0d1c741 100644 --- a/src/app/code-create/page.tsx +++ b/src/app/code-create/page.tsx @@ -47,6 +47,30 @@ export default function CodeCreatePage() {
+ {/* Registration CTA - Featured First */} +
+
+
+ +
+

+ Register Now for 2026! +

+

+ Join us for Code & Create 2026! High school students will learn programming, + build amazing projects, and explore the University of Toledo College of Engineering. +

+ + Register Now - It's Free! + +
+
+ {/* Main Heading */}

@@ -81,20 +105,21 @@ export default function CodeCreatePage() { {/* Call to Action */}
-
+

- Ready to Join Next Year? + Don't Miss Out!

- Code & Create 2026 is coming soon! Fill out our interest form to be notified when registration opens. + Spaces are filling up fast! Register now for Code & Create 2026 and secure your spot + for an amazing day of learning and creativity.

- Fill Interest Form + Register Now
@@ -274,12 +299,12 @@ export default function CodeCreatePage() {

- Fill Interest Form + Register Now Date: Tue, 27 Jan 2026 22:42:04 -0500 Subject: [PATCH 03/24] Added RH Arrow Favicon - Aadi --- .../rh_26_arrow_transparent.png | Bin 0 -> 69499 bytes src/app/layout.tsx | 24 +++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 public/assets/rh_26/rh_26_folder/rh_26_bundle_png/rh_26_arrow_transparent.png 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 0000000000000000000000000000000000000000..965cba4909d54e084d0b6d71acb7fac763fa3767 GIT binary patch literal 69499 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 gzyJ3g;0QUQH+2^uy6haI1l>hVSxc$t{ From 6ff980ccae4efa1573205116ae000eb4f3ccc1b5 Mon Sep 17 00:00:00 2001 From: aadinathsj Date: Fri, 30 Jan 2026 13:11:13 -0500 Subject: [PATCH 04/24] Application Status Check + Scroll bar on Admin Portal --- DEPLOYMENT_IOS_FIX.md | 189 -------- DEPLOYMENT_READY.md | 343 -------------- DOCUMENTATION_INDEX.md | 174 ------- FEATURES_BUILT.md | 473 ------------------- IOS_COOKIE_FIX_V2.md | 216 --------- IOS_MAGIC_LINK_FIX.md | 255 ---------- MAGIC_LINK_RETRY_FIX.md | 336 -------------- PASSWORD_AUTH_MIGRATION.md | 563 ----------------------- PASSWORD_RESET_FIX.md | 309 ------------- PASSWORD_SETUP_FIX.md | 227 --------- PERFORMANCE_OPTIMIZATION.md | 167 ------- README_UPDATES.md | 296 ------------ SETUP_COMPLETE.md | 443 ------------------ package-lock.json | 86 ++-- src/app/admin/page.tsx | 420 ++++++++++------- src/app/globals.css | 27 ++ src/app/organizer/page.tsx | 272 ++++++----- src/lib/utils/applicationCompleteness.ts | 74 +-- 18 files changed, 533 insertions(+), 4337 deletions(-) delete mode 100644 DEPLOYMENT_IOS_FIX.md delete mode 100644 DEPLOYMENT_READY.md delete mode 100644 DOCUMENTATION_INDEX.md delete mode 100644 FEATURES_BUILT.md delete mode 100644 IOS_COOKIE_FIX_V2.md delete mode 100644 IOS_MAGIC_LINK_FIX.md delete mode 100644 MAGIC_LINK_RETRY_FIX.md delete mode 100644 PASSWORD_AUTH_MIGRATION.md delete mode 100644 PASSWORD_RESET_FIX.md delete mode 100644 PASSWORD_SETUP_FIX.md delete mode 100644 PERFORMANCE_OPTIMIZATION.md delete mode 100644 README_UPDATES.md delete mode 100644 SETUP_COMPLETE.md diff --git a/DEPLOYMENT_IOS_FIX.md b/DEPLOYMENT_IOS_FIX.md deleted file mode 100644 index 45e6e6d..0000000 --- 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 8fe8a74..0000000 --- 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 77bb7a4..0000000 --- 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 8d5c940..0000000 --- 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 8e3d6e8..0000000 --- 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 8744dae..0000000 --- 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 5536898..0000000 --- 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 421fac5..0000000 --- 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 8b0438a..0000000 --- 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 fe6de59..0000000 --- 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 211ef6d..0000000 --- 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 81f2486..0000000 --- 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 c7886f7..0000000 --- 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 4c7f5aa..8549cd4 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/src/app/admin/page.tsx b/src/app/admin/page.tsx index 1587406..0f1cdef 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,181 +1,181 @@ -'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 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"; default: - return 'bg-gray-500/20 text-gray-400' + return "bg-gray-500/20 text-gray-400"; } - } + }; 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, + 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 @@ -193,25 +193,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 +221,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
@@ -291,13 +300,14 @@ export default function AdminPage() {
- Showing {filteredApplications.length} of {applications.length} applications + Showing {filteredApplications.length} of {applications.length}{" "} + applications
{/* Applications Table */}
-
+
@@ -312,16 +322,27 @@ export default function AdminPage() { {filteredApplications.map((app) => ( - + - - + + @@ -352,20 +373,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 +424,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 +508,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 +537,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 +562,25 @@ export default function AdminPage() { {/* Status Update */}
)}
- ) + ); } diff --git a/src/app/globals.css b/src/app/globals.css index b048a68..f895e7f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -53,6 +53,33 @@ background: var(--primary-purple-light); } +/* Custom scrollbar for tables and other containers */ +.custom-scrollbar::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +.custom-scrollbar::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + border-radius: 10px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.15); + border-radius: 10px; + border: 2px solid rgba(255, 255, 255, 0.05); +} + +.custom-scrollbar::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.25); +} + +/* Firefox scrollbar styling */ +.custom-scrollbar { + scrollbar-color: rgba(255, 255, 255, 0.15) rgba(255, 255, 255, 0.05); + scrollbar-width: thin; +} + /* Reset and base responsive setup */ *, *::before, diff --git a/src/app/organizer/page.tsx b/src/app/organizer/page.tsx index ddd0899..dab6dde 100644 --- a/src/app/organizer/page.tsx +++ b/src/app/organizer/page.tsx @@ -1,114 +1,114 @@ -'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 type { Applicant } from '@/types/database' +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { createClient } from "@/lib/supabase/client"; +import Link from "next/link"; +import type { Applicant } from "@/types/database"; export default function OrganizerPortal() { - const router = useRouter() - const supabase = createClient() - const [loading, setLoading] = useState(true) - const [applicants, setApplicants] = useState([]) - const [stats, setStats] = useState(null) - const [searchTerm, setSearchTerm] = useState('') - const [checkinFilter, setCheckinFilter] = useState('all') - const [selectedApp, setSelectedApp] = useState(null) - const [checkingIn, setCheckingIn] = useState(null) + const router = useRouter(); + const supabase = createClient(); + const [loading, setLoading] = useState(true); + const [applicants, setApplicants] = useState([]); + const [stats, setStats] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); + const [checkinFilter, setCheckinFilter] = useState("all"); + const [selectedApp, setSelectedApp] = useState(null); + const [checkingIn, setCheckingIn] = useState(null); useEffect(() => { async function checkOrganizerAndLoadData() { // Check if user is organizer or admin - const authResponse = await fetch('/api/auth/user') - const authData = await authResponse.json() + const authResponse = await fetch("/api/auth/user"); + const authData = await authResponse.json(); if (!authData.isOrganizer && !authData.isAdmin) { - router.push('/dashboard') - return + router.push("/dashboard"); + return; } - await loadApplicants() - await loadStats() - setLoading(false) + await loadApplicants(); + await loadStats(); + setLoading(false); } - checkOrganizerAndLoadData() - }, []) + checkOrganizerAndLoadData(); + }, []); const loadApplicants = async () => { // Only load accepted applicants for check-in const { data, error } = await supabase - .from('applicants') - .select('*') - .eq('status', 'accepted') - .order('last_name', { ascending: true }) + .from("applicants") + .select("*") + .eq("status", "accepted") + .order("last_name", { ascending: true }); if (error) { - console.error('Error loading applicants:', error) + console.error("Error loading applicants:", error); } else { - setApplicants((data as Applicant[]) || []) + setApplicants((data as Applicant[]) || []); } - } + }; const loadStats = async () => { - const response = await fetch('/api/organizer/checkin') + const response = await fetch("/api/organizer/checkin"); if (response.ok) { - const data = await response.json() - setStats(data.stats) + const data = await response.json(); + setStats(data.stats); } - } + }; const handleCheckIn = async (applicantId: string, currentStatus: boolean) => { - setCheckingIn(applicantId) + setCheckingIn(applicantId); - const response = await fetch('/api/organizer/checkin', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + const response = await fetch("/api/organizer/checkin", { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ applicant_id: applicantId, - checked_in: !currentStatus - }) - }) + checked_in: !currentStatus, + }), + }); if (response.ok) { - await loadApplicants() - await loadStats() - setSelectedApp(null) + await loadApplicants(); + await loadStats(); + setSelectedApp(null); } else { - const error = await response.json() - alert(`Error: ${error.error}`) + const error = await response.json(); + alert(`Error: ${error.error}`); } - setCheckingIn(null) - } + setCheckingIn(null); + }; const handleLogout = async () => { - await supabase.auth.signOut() - router.push('/') - } + await supabase.auth.signOut(); + router.push("/"); + }; - const filteredApplicants = applicants.filter(app => { + const filteredApplicants = applicants.filter((app) => { const matchesCheckin = - checkinFilter === 'all' || - (checkinFilter === 'checked-in' && app.checked_in) || - (checkinFilter === 'not-checked-in' && !app.checked_in) + checkinFilter === "all" || + (checkinFilter === "checked-in" && app.checked_in) || + (checkinFilter === "not-checked-in" && !app.checked_in); - const fullName = `${app.first_name} ${app.last_name}`.toLowerCase() + const fullName = `${app.first_name} ${app.last_name}`.toLowerCase(); const matchesSearch = - searchTerm === '' || + searchTerm === "" || fullName.includes(searchTerm.toLowerCase()) || app.email.toLowerCase().includes(searchTerm.toLowerCase()) || - app.school.toLowerCase().includes(searchTerm.toLowerCase()) + app.school.toLowerCase().includes(searchTerm.toLowerCase()); - return matchesCheckin && matchesSearch - }) + return matchesCheckin && matchesSearch; + }); if (loading) { return (
Loading...
- ) + ); } return ( @@ -117,8 +117,12 @@ export default function OrganizerPortal() { {/* Header */}
-

Check-In Portal

-

RocketHacks 2026 Participant Check-In

+

+ Check-In Portal +

+

+ RocketHacks 2026 Participant Check-In +

-
Total Accepted
-
{stats.total_accepted || 0}
+
+ Total Accepted +
+
+ {stats.total_accepted || 0} +
-
Checked In
-
{stats.checked_in_count || 0}
+
+ Checked In +
+
+ {stats.checked_in_count || 0} +
-
Not Checked In
-
{stats.not_checked_in_count || 0}
+
+ Not Checked In +
+
+ {stats.not_checked_in_count || 0} +
-
Check-In Rate
-
{stats.checkin_percentage || 0}%
+
+ Check-In Rate +
+
+ {stats.checkin_percentage || 0}% +
)} @@ -179,13 +199,14 @@ export default function OrganizerPortal() {
- Showing {filteredApplicants.length} of {applicants.length} accepted participants + Showing {filteredApplicants.length} of {applicants.length} accepted + participants
{/* Applicants Table */}
-
+
-
{app.first_name} {app.last_name}
+
+ {app.first_name} {app.last_name} +
{app.email}
{app.school}{app.major || '-'}{app.age || '-'} + {app.major || "-"} + + {app.age || "-"} + - + {app.status}
@@ -198,13 +219,20 @@ export default function OrganizerPortal() { {filteredApplicants.map((app) => ( - + - + @@ -260,16 +292,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 +325,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 +384,26 @@ export default function OrganizerPortal() { {/* Check-In Button */}
)}
- ) + ); } diff --git a/src/lib/utils/applicationCompleteness.ts b/src/lib/utils/applicationCompleteness.ts index 4bc5151..00cbccc 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 From b880d863983edb5862c290005eec463d03d023c0 Mon Sep 17 00:00:00 2001 From: aadinathsj Date: Sun, 8 Feb 2026 00:57:21 -0500 Subject: [PATCH 05/24] - FAQ Fixed - Discord Inv Added - About Page Fixed - Code and Create Join Us Fixed - MLH Tag removed in Phone View --- public/assets/rh_26/discord-black-icon.png | Bin 0 -> 10659 bytes src/app/code-create/page.tsx | 285 ++++++-- src/app/dashboard/page.tsx | 769 +++++++++++++++------ src/app/layout.tsx | 3 +- src/components/about/About.tsx | 135 ++-- src/components/faq/Faq.tsx | 149 ++-- 6 files changed, 979 insertions(+), 362 deletions(-) create mode 100644 public/assets/rh_26/discord-black-icon.png 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 0000000000000000000000000000000000000000..5e5caccffca34e33cdcf9d49791d9558d5a7df36 GIT binary patch literal 10659 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 literal 0 HcmV?d00001 diff --git a/src/app/code-create/page.tsx b/src/app/code-create/page.tsx index 0d1c741..fe9a23e 100644 --- a/src/app/code-create/page.tsx +++ b/src/app/code-create/page.tsx @@ -3,7 +3,13 @@ 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"; @@ -14,7 +20,10 @@ export default function CodeCreatePage() {

- High school developers showcasing their creativity during Code & Create 2025 + High school developers showcasing their creativity during Code & + Create 2025

@@ -106,12 +122,15 @@ export default function CodeCreatePage() { {/* Call to Action */}
-

+

Don't Miss Out!

- Spaces are filling up fast! Register now for Code & Create 2026 and secure your spot - for an amazing day of learning and creativity. + Spaces are filling up fast! Register now for Code & Create 2026 + and secure your spot for an amazing day of learning and + creativity.

-
-
-
+
+
+
{/* Main Agenda Section */}
-

+

Event Highlights

@@ -148,12 +178,16 @@ export default function CodeCreatePage() {
-

+

KEYNOTE SESSION

- Tom Bush, CEO of ActualReality, inspired students to embrace creativity and innovation. - He focused on real-world tech applications and encouraged thinking like creators, not just consumers. + Tom Bush, CEO of ActualReality, inspired students to embrace + creativity and innovation. He focused on real-world tech + applications and encouraged thinking like creators, not just + consumers.

@@ -162,12 +196,15 @@ export default function CodeCreatePage() {
-

+

CAMPUS TOUR

- Guided exploration of the College of Engineering, featuring research labs, student organizations, - and demonstrations by the Rocketry Club and Formula SAE Team. + Guided exploration of the College of Engineering, featuring + research labs, student organizations, and demonstrations by the + Rocketry Club and Formula SAE Team.

@@ -176,26 +213,35 @@ export default function CodeCreatePage() {
-

+

CODING WORKSHOP

- Partnering with KoolKatScience, students learned programming through Scratch, - covering logic, loops, and interactivity in a creative, beginner-friendly way. + Partnering with KoolKatScience, students learned programming + through Scratch, covering logic, loops, and interactivity in a + creative, beginner-friendly way.

{/* Project Development */}
- +
-

+

PROJECT BUILD

- Students brainstormed and built interactive projects using Scratch. - The RocketHacks Tech Team provided one-on-one mentorship for debugging and refinement. + Students brainstormed and built interactive projects using + Scratch. The RocketHacks Tech Team provided one-on-one + mentorship for debugging and refinement.

@@ -204,12 +250,15 @@ export default function CodeCreatePage() {
-

+

MENTORSHIP

- University mentors provided guidance and support throughout the development process, - helping participants debug, refine, and polish their creative ideas. + University mentors provided guidance and support throughout the + development process, helping participants debug, refine, and + polish their creative ideas.

@@ -218,12 +267,15 @@ export default function CodeCreatePage() {
-

+

AWARDS CEREMONY

- Students presented final projects to judges. Dr. Sammy Spann congratulated participants, - with Nehemia winning for a multi-level interactive game with over 40 stages! + Students presented final projects to judges. Dr. Sammy Spann + congratulated participants, with Nehemia winning for a + multi-level interactive game with over 40 stages!

@@ -236,19 +288,30 @@ export default function CodeCreatePage() {
{/* Left Column - Benefits */}
-

+

Why Join Code & Create?

- +
- + + ✓ +
-

Beginner-Friendly

-

Perfect for students of all backgrounds - no prior experience needed!

+

+ Beginner-Friendly +

+

+ Perfect for students of all backgrounds - no prior + experience needed! +

@@ -257,8 +320,15 @@ export default function CodeCreatePage() {
-

Learn by Doing

-

Create your own game or interactive project with immediate results.

+

+ Learn by Doing +

+

+ Create your own game or interactive project with immediate + results. +

@@ -267,8 +337,15 @@ export default function CodeCreatePage() {
-

College Exposure

-

Experience university life, meet students and professors, explore labs.

+

+ College Exposure +

+

+ Experience university life, meet students and professors, + explore labs. +

@@ -277,8 +354,15 @@ export default function CodeCreatePage() {
-

Build Confidence

-

Develop practical skills in programming, creativity, and problem-solving.

+

+ Build Confidence +

+

+ Develop practical skills in programming, creativity, and + problem-solving. +

@@ -288,14 +372,21 @@ export default function CodeCreatePage() {
- +
-

- Join Us Next Year! +

+ Join Us!

- Code & Create 2026 is in development! Fill out our interest form to be the first to know when registration opens - for another amazing day of learning, building, and celebrating creativity. + Code & Create 2026 is in development! Fill out our interest + form to be the first to know when registration opens for + another amazing day of learning, building, and celebrating + creativity.

-

+

What Students Gained

- More than just coding - students walked away with confidence, skills, and inspiration for their future. + More than just coding - students walked away with confidence, + skills, and inspiration for their future.

-

Hands-On Experience

-

Solid introduction to programming and logical thinking through project-based learning.

+

+ Hands-On Experience +

+

+ Solid introduction to programming and logical thinking through + project-based learning. +

-

Creative Problem Solving

-

Learned to turn imaginative ideas into functional digital projects.

+

+ Creative Problem Solving +

+

+ Learned to turn imaginative ideas into functional digital + projects. +

-

Innovation Mindset

-

Understanding how technology enables innovation and exploration.

+

+ Innovation Mindset +

+

+ Understanding how technology enables innovation and exploration. +

-

Mentorship & Collaboration

-

Direct guidance from university mentors throughout the process.

+

+ Mentorship & Collaboration +

+

+ Direct guidance from university mentors throughout the process. +

-

College Preparation

-

Exposure to student organizations, labs, and research opportunities.

+

+ College Preparation +

+

+ Exposure to student organizations, labs, and research + opportunities. +

-

Presentation Skills

-

Practice articulating technical ideas to judges and peers.

+

+ Presentation Skills +

+

+ Practice articulating technical ideas to judges and peers. +

@@ -385,4 +518,4 @@ export default function CodeCreatePage() {
); -} \ No newline at end of file +} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index dcca884..a1dc623 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,82 +1,91 @@ -'use client' - -import { useState, useEffect } from 'react' -import { useRouter } from 'next/navigation' -import { createClient } from '@/lib/supabase/client' -import Link from 'next/link' -import Image from 'next/image' -import { terminal } from '../fonts/fonts' -import { checkApplicationCompleteness, groupMissingFieldsByCategory, getCategoryDisplayName, type CompletenessResult } from '@/lib/utils/applicationCompleteness' +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { createClient } from "@/lib/supabase/client"; +import Link from "next/link"; +import Image from "next/image"; +import { terminal } from "../fonts/fonts"; +import { + checkApplicationCompleteness, + groupMissingFieldsByCategory, + getCategoryDisplayName, + type CompletenessResult, +} from "@/lib/utils/applicationCompleteness"; export default function DashboardPage() { - const router = useRouter() - const supabase = createClient() - const [loading, setLoading] = useState(true) - const [user, setUser] = useState(null) - const [application, setApplication] = useState(null) - const [isAdmin, setIsAdmin] = useState(false) - const [isOrganizer, setIsOrganizer] = useState(false) - const [completeness, setCompleteness] = useState(null) - const [showMissingFields, setShowMissingFields] = useState(false) + const router = useRouter(); + const supabase = createClient(); + const [loading, setLoading] = useState(true); + const [user, setUser] = useState(null); + const [application, setApplication] = useState(null); + const [isAdmin, setIsAdmin] = useState(false); + const [isOrganizer, setIsOrganizer] = useState(false); + const [completeness, setCompleteness] = useState( + null, + ); + const [showMissingFields, setShowMissingFields] = useState(false); useEffect(() => { async function loadData() { // Get user - const { data: { user } } = await supabase.auth.getUser() + const { + data: { user }, + } = await supabase.auth.getUser(); if (!user) { - router.push('/login?redirect=/dashboard') - return + router.push("/login?redirect=/dashboard"); + return; } - setUser(user) + setUser(user); // Check if user is admin or organizer - const authResponse = await fetch('/api/auth/user') - const authData = await authResponse.json() - setIsAdmin(authData.isAdmin) - setIsOrganizer(authData.isOrganizer) + const authResponse = await fetch("/api/auth/user"); + const authData = await authResponse.json(); + setIsAdmin(authData.isAdmin); + setIsOrganizer(authData.isOrganizer); // Get application const { data: app, error } = await supabase - .from('applicants') - .select('*') - .eq('user_id', user.id) - .maybeSingle() + .from("applicants") + .select("*") + .eq("user_id", user.id) + .maybeSingle(); if (error) { - console.error('Error loading application:', error) + console.error("Error loading application:", error); } else { - setApplication(app) + setApplication(app); // Check completeness if application exists if (app) { - const completenessResult = checkApplicationCompleteness(app) - setCompleteness(completenessResult) + const completenessResult = checkApplicationCompleteness(app); + setCompleteness(completenessResult); } } - setLoading(false) + setLoading(false); } - loadData() - }, []) + loadData(); + }, []); const handleLogout = async () => { - await supabase.auth.signOut() - router.push('/') - } + await supabase.auth.signOut(); + router.push("/"); + }; const getStatusColor = (status: string) => { switch (status) { - case 'pending': - return 'bg-rh-yellow/20 text-rh-yellow border-rh-yellow/50' - case 'accepted': - return 'bg-green-500/20 text-green-400 border-green-500/50' - case 'rejected': - return 'bg-red-500/20 text-red-400 border-red-500/50' - case 'waitlisted': - return 'bg-rh-purple-light/20 text-rh-purple-light border-rh-purple-light/50' + case "pending": + return "bg-rh-yellow/20 text-rh-yellow border-rh-yellow/50"; + case "accepted": + return "bg-green-500/20 text-green-400 border-green-500/50"; + case "rejected": + return "bg-red-500/20 text-red-400 border-red-500/50"; + case "waitlisted": + return "bg-rh-purple-light/20 text-rh-purple-light border-rh-purple-light/50"; default: - return 'bg-gray-500/20 text-gray-400 border-gray-500/50' + return "bg-gray-500/20 text-gray-400 border-gray-500/50"; } - } + }; if (loading) { return ( @@ -93,7 +102,7 @@ export default function DashboardPage() {
Loading...
- ) + ); } return ( @@ -113,16 +122,27 @@ export default function DashboardPage() { {/* Floating Elements */}
-
-
-
+
+
+
{/* Header */}
-

+

Dashboard

Welcome back, {user?.email}

@@ -156,31 +176,41 @@ export default function DashboardPage() { {/* Application Status */} {application ? ( -
+
{/* Application Completeness Card */} {completeness && (
-

- {completeness.isComplete ? '✓ Application Complete' : '⚠️ Application Incomplete'} +

+ {completeness.isComplete + ? "✓ Application Complete" + : "⚠️ Application Incomplete"}

{!completeness.isComplete && ( - ({completeness.filledFields}/{completeness.totalFields} fields) + ({completeness.filledFields}/ + {completeness.totalFields} fields) )}

{completeness.isComplete - ? 'Great! Your application is 100% complete.' + ? "Great! Your application is 100% complete." : `You're ${completeness.percentage}% complete. Fill out all fields to improve your chances!`}

-
+
{completeness.percentage}%
@@ -189,7 +219,7 @@ export default function DashboardPage() { onClick={() => setShowMissingFields(!showMissingFields)} className="btn-secondary px-4 py-2 text-sm font-semibold whitespace-nowrap" > - {showMissingFields ? 'Hide' : 'Show'} Missing Fields + {showMissingFields ? "Hide" : "Show"} Missing Fields )}
@@ -200,8 +230,8 @@ export default function DashboardPage() {
@@ -211,17 +241,33 @@ export default function DashboardPage() { {!completeness.isComplete && showMissingFields && (
-

+

Missing Fields ({completeness.missingFields.length})

- {Object.entries(groupMissingFieldsByCategory(completeness.missingFields)).map(([category, fields]) => ( -
-
{getCategoryDisplayName(category)}
+ {Object.entries( + groupMissingFieldsByCategory( + completeness.missingFields, + ), + ).map(([category, fields]) => ( +
+
+ {getCategoryDisplayName(category)} +
- + @@ -336,8 +344,12 @@ export default function AdminPage() { -
-
{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 +264,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"}
Name School MajorAgeRSVP Status Submitted Actions {app.major || "-"} - {app.age || "-"} + + + {!!app.rsvp_attending ? "Yes" : "No"} + (null); const [isAdmin, setIsAdmin] = useState(false); const [isOrganizer, setIsOrganizer] = useState(false); + const [rsvpUpdating, setRsvpUpdating] = useState(null); + const [rsvpError, setRsvpError] = useState(null); const [completeness, setCompleteness] = useState( null, ); @@ -84,6 +86,28 @@ export default function DashboardPage() { router.push("/"); }; + const updateRsvp = async (attending: boolean) => { + if (!application?.id) return; + setRsvpError(null); + setRsvpUpdating(attending); + const { error } = await supabase + .from("applicants") + .update({ rsvp_attending: attending }) + .eq("id", application.id); + + if (error) { + console.error("Error updating RSVP:", error); + setRsvpError("Failed to update RSVP. Please try again."); + setRsvpUpdating(null); + return; + } + + setApplication((prev: any) => + prev ? { ...prev, rsvp_attending: attending } : prev, + ); + setRsvpUpdating(null); + }; + const getStatusColor = (status: string) => { switch (status) { case "pending": @@ -202,8 +226,8 @@ export default function DashboardPage() { className={`${terminal.className} text-xl font-semibold uppercase tracking-wide ${completeness.isComplete ? "text-green-400" : "text-yellow-400"}`} > {completeness.isComplete - ? "✓ Application Complete" - : "⚠️ Application Incomplete"} + ? "Application Complete" + : "Application Incomplete"} {!completeness.isComplete && ( @@ -376,7 +400,7 @@ export default function DashboardPage() { Accepted!

- 🎉 Congratulations! You're in! + Congratulations! You're in!

@@ -502,7 +526,7 @@ export default function DashboardPage() { > {application.checked_in ? "✓ Verified & Active" - : "⏳ Pending Check-In"} + : "Pending Check-In"}

@@ -615,112 +639,100 @@ export default function DashboardPage() { - {/* Hacker Q&A Session Card */} - -
+
-
- - - -
-
-

- Hacker Q&A Session -

-

- Meet the organizing team and ask any questions! -

-
-
    -
  • - - - Learn about event schedule and expectations - -
  • -
  • - - - Ask about projects, teams, and resources - -
  • -
  • - - Chat with the organizing team -
  • -
-
- - - - - Monday, March 9 - - - +
+
+
+ + + +
+
+

+ RSVP Required +

+

+ March 14–15, 2026 (Saturday–Sunday) +

+
+
+ +

+ Will you be present for RocketHacks on March 14 and + 15, 2026? +

+ +
+ {application?.rsvp_attending && ( + + RSVP Confirmed + + )} + {rsvpError && ( + + {rsvpError} + + )} +
+ +
+
-
- - + - Join Teams Meeting - + No, I will not be there + +
+ + {rsvpUpdating !== null && ( +

+ Saving your RSVP… +

+ )}
-
+ +
-
+
{/* Intro to AWS Cloud Services Workshop Card */} Date: Wed, 11 Mar 2026 23:31:07 -0400 Subject: [PATCH 18/24] RSVP Form added --- src/app/admin/page.tsx | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 12fbf10..5328b3b 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -35,6 +35,19 @@ export default function AdminPage() { 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") @@ -129,6 +142,8 @@ export default function AdminPage() { 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"; } @@ -257,7 +272,7 @@ export default function AdminPage() { {/* Stats */} {stats && stats.length > 0 && ( -
+
{stats.map((stat: any) => (
))} + {/* RSVP Card */} + {applications.length > 0 && ( +
+
+ RSVP +
+
+ {calculateRsvpStats().count} +
+
+ {calculateRsvpStats().first_time_hackers} first-timers +
+
+ )}
)} From b3920479cafad099d81c896af2c9ee7084051a2c Mon Sep 17 00:00:00 2001 From: aadinathsj Date: Thu, 12 Mar 2026 16:25:06 -0400 Subject: [PATCH 19/24] Zoom call AWS --- src/app/dashboard/page.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 8246739..5b98652 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -703,7 +703,9 @@ export default function DashboardPage() { onClick={() => updateRsvp(true)} disabled={rsvpUpdating !== null} className={`btn-primary px-6 py-3 text-sm font-semibold w-full ${ - application?.rsvp_attending ? "ring-2 ring-green-400/40" : "" + application?.rsvp_attending + ? "ring-2 ring-green-400/40" + : "" }`} > Yes, I will be there @@ -736,7 +738,7 @@ export default function DashboardPage() { {/* Intro to AWS Cloud Services Workshop Card */}
- Join Teams Meeting + Join Zoom Call
From 256fa6c5b7f46cfba597b3aff474057e42b630ba Mon Sep 17 00:00:00 2001 From: aadinathsj Date: Sat, 14 Mar 2026 10:39:06 -0400 Subject: [PATCH 20/24] Tech Together Update --- src/components/schedule/Schedule.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/schedule/Schedule.tsx b/src/components/schedule/Schedule.tsx index 39c3bdb..82ba3a7 100644 --- a/src/components/schedule/Schedule.tsx +++ b/src/components/schedule/Schedule.tsx @@ -54,12 +54,6 @@ const scheduleData = [ endTime: "1:00 PM", location: "NE 1320", }, - { - name: "Tech Together - MLH Workshop", - startTime: "12:30 AM", - endTime: "1:00 PM", - location: "NE 1021", - }, { name: "Intro to Google AI Studio - MLH Workshop", startTime: "1:30 PM", @@ -78,6 +72,12 @@ const scheduleData = [ endTime: "4:00 PM", location: "NE 1039", }, + { + name: "Tech Together - MLH Workshop", + startTime: "4:15 PM", + endTime: "4:45 PM", + location: "NE 1021", + }, { name: "Dinner", startTime: "8:00 PM", From daad05f2461fc3442e498424aea075b3bd992cc7 Mon Sep 17 00:00:00 2001 From: aadinathsj Date: Sat, 14 Mar 2026 13:21:31 -0400 Subject: [PATCH 21/24] Room Change --- src/components/schedule/Schedule.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/schedule/Schedule.tsx b/src/components/schedule/Schedule.tsx index 82ba3a7..391b88e 100644 --- a/src/components/schedule/Schedule.tsx +++ b/src/components/schedule/Schedule.tsx @@ -58,7 +58,7 @@ const scheduleData = [ name: "Intro to Google AI Studio - MLH Workshop", startTime: "1:30 PM", endTime: "2:00 PM", - location: "NE 1039", + location: "SSOE", }, { name: "From Prompt to Agent: Creating an OpenClaw AI Agent - CodeEcho Workshop", From 32d160c951ba688144ce8ede33553b861f331f80 Mon Sep 17 00:00:00 2001 From: aadinathsj Date: Sat, 14 Mar 2026 14:10:41 -0400 Subject: [PATCH 22/24] Remove ART Workshop --- src/components/schedule/Schedule.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/components/schedule/Schedule.tsx b/src/components/schedule/Schedule.tsx index 391b88e..d8bef87 100644 --- a/src/components/schedule/Schedule.tsx +++ b/src/components/schedule/Schedule.tsx @@ -66,12 +66,6 @@ const scheduleData = [ endTime: "3:00 PM", location: "NE 1300", }, - { - name: "Ship It: Build Real Software with AI in One Hackathon - A.R.T. Workshop", - startTime: "3:00 PM", - endTime: "4:00 PM", - location: "NE 1039", - }, { name: "Tech Together - MLH Workshop", startTime: "4:15 PM", From 342a27949510b87bf37cd11a8d9ce55aed66fffe Mon Sep 17 00:00:00 2001 From: aadinathsj Date: Sun, 15 Mar 2026 08:42:29 -0400 Subject: [PATCH 23/24] Export Excel in CheckIn --- src/app/organizer/page.tsx | 47 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/src/app/organizer/page.tsx b/src/app/organizer/page.tsx index dab6dde..94f5e0c 100644 --- a/src/app/organizer/page.tsx +++ b/src/app/organizer/page.tsx @@ -4,6 +4,7 @@ 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 type { Applicant } from "@/types/database"; export default function OrganizerPortal() { @@ -19,7 +20,6 @@ export default function OrganizerPortal() { useEffect(() => { async function checkOrganizerAndLoadData() { - // Check if user is organizer or admin const authResponse = await fetch("/api/auth/user"); const authData = await authResponse.json(); @@ -36,7 +36,6 @@ export default function OrganizerPortal() { }, []); const loadApplicants = async () => { - // Only load accepted applicants for check-in const { data, error } = await supabase .from("applicants") .select("*") @@ -87,6 +86,44 @@ export default function OrganizerPortal() { router.push("/"); }; + const exportToExcel = () => { + const data = filteredApplicants.map((app) => ({ + "First Name": app.first_name || "", + "Last Name": app.last_name || "", + Age: app.age || "", + Email: app.email || "", + School: app.school || "", + "Phone Number": app.phone_number || "", + Country: app.country_of_residence || "", + "Level of Study": app.level_of_study || "", + "Checked In": app.checked_in ? "Yes" : "No", + "Checked In At": app.checked_in_at + ? new Date(app.checked_in_at).toLocaleString() + : "", + })); + + const worksheet = XLSX.utils.json_to_sheet(data); + + worksheet["!cols"] = [ + { wch: 15 }, // First Name + { wch: 15 }, // Last Name + { wch: 8 }, // Age + { wch: 30 }, // Email + { wch: 35 }, // School + { wch: 15 }, // Phone Number + { wch: 20 }, // Country + { wch: 18 }, // Level of Study + { wch: 12 }, // Checked In + { wch: 22 }, // Checked In At + ]; + + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, worksheet, "Check-Ins"); + + const fileName = `RocketHacks_2026_CheckIns_${new Date().toISOString().split("T")[0]}.xlsx`; + XLSX.writeFile(workbook, fileName); + }; + const filteredApplicants = applicants.filter((app) => { const matchesCheckin = checkinFilter === "all" || @@ -197,6 +234,12 @@ export default function OrganizerPortal() { +
Showing {filteredApplicants.length} of {applicants.length} accepted From 0f0119250f75488464c5e0472dc3bf6a721cbfd8 Mon Sep 17 00:00:00 2001 From: aadinathsj Date: Wed, 25 Mar 2026 21:56:33 -0400 Subject: [PATCH 24/24] Website cleanup --- src/app/code-create/page.tsx | 236 +++++++++++-------------------- src/app/page.tsx | 6 - src/components/about/About.tsx | 102 +------------ src/components/footer/Footer.tsx | 1 - src/components/hero/Hero.tsx | 143 ++++++++++--------- src/components/navbar/Navbar.tsx | 3 - 6 files changed, 157 insertions(+), 334 deletions(-) diff --git a/src/app/code-create/page.tsx b/src/app/code-create/page.tsx index c76f7c8..10ac5dc 100644 --- a/src/app/code-create/page.tsx +++ b/src/app/code-create/page.tsx @@ -15,9 +15,17 @@ import { GiBrain } from "react-icons/gi"; export default function CodeCreatePage() { return ( -
+
+ {/* Unified background pattern (prevents segmented/sectioned look) */} +
{/* Navigation Bar - Simple version */} -