From 7afa81377267627a7b84391d0596223178cb908b Mon Sep 17 00:00:00 2001 From: Ethan Date: Sun, 28 Jun 2026 22:46:14 -0700 Subject: [PATCH 01/12] feat(email-verification): add email_verified field to users --- .../f250cb93a643_add_email_verified.py | 26 +++++++++++++++++++ backend/app/models/models.py | 1 + 2 files changed, 27 insertions(+) create mode 100644 backend/alembic/versions/f250cb93a643_add_email_verified.py diff --git a/backend/alembic/versions/f250cb93a643_add_email_verified.py b/backend/alembic/versions/f250cb93a643_add_email_verified.py new file mode 100644 index 00000000..f39b83c9 --- /dev/null +++ b/backend/alembic/versions/f250cb93a643_add_email_verified.py @@ -0,0 +1,26 @@ +"""add_email_verified + +Revision ID: f250cb93a643 +Revises: 2e1dda44cde5 +Create Date: 2026-06-28 22:31:08.365083 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'f250cb93a643' +down_revision: Union[str, None] = '2e1dda44cde5' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column('users', sa.Column('email_verified', sa.Boolean(), nullable=False, server_default=sa.text('false'))) + + +def downgrade() -> None: + op.drop_column('users', 'email_verified') diff --git a/backend/app/models/models.py b/backend/app/models/models.py index 569e3e05..a75e5f04 100644 --- a/backend/app/models/models.py +++ b/backend/app/models/models.py @@ -91,6 +91,7 @@ class User(Base): # Auth fields hashed_password = Column(String(255), nullable=True) # null = cannot log in, must reset password and verify via email + email_verified = Column(Boolean, nullable=False, default=False) role = Column(String(32), nullable=False, default="user") # "admin" | "user" is_active = Column(Boolean, nullable=False, default=True) From c247918e904342193a414c66078554826527aeb3 Mon Sep 17 00:00:00 2001 From: Ethan Date: Sun, 28 Jun 2026 23:00:50 -0700 Subject: [PATCH 02/12] feat(email_verification): add RESENT_API_KEY and FRONTEND_URL env vars; add helper functions to generate JWT email verification tokens --- backend/app/core/config.py | 7 +++++-- backend/app/core/email_verification.py | 27 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 backend/app/core/email_verification.py diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 940957fe..52f02921 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -10,16 +10,19 @@ class Settings(BaseSettings): app_host: str = "0.0.0.0" app_port: int = 8000 - database_url: str = "sqlite:///./nexus.db" + database_url: str = "postgresql://nexus:nexus@127.0.0.1:5432/nexus" google_service_account_file: str = "./credentials.json" google_service_account_json: str = "" # JSON string — used in production instead of file api_key: str = "" # For direct API access / Swagger only - # Must be set to a long random string in production — never commit the real value + # Must be set to a long random string in production jwt_secret: str = "dev-secret-change-in-production" + resend_api_key: str = "" # set in .env file for dev or env vars in prod, never commit here + frontend_url: str = "http://localhost:3000/" # remember to set to actual url in prod + @lru_cache() def get_settings() -> Settings: diff --git a/backend/app/core/email_verification.py b/backend/app/core/email_verification.py new file mode 100644 index 00000000..ca4d8ed7 --- /dev/null +++ b/backend/app/core/email_verification.py @@ -0,0 +1,27 @@ +from datetime import datetime, timedelta, timezone +from typing import Optional + +from jose import JWTError, jwt + +from app.core.config import get_settings + +ALGORITHM = "HS256" +VERIFICATION_TOKEN_EXPIRE_DAYS = 1 + +def generate_verification_token(user_id: int) -> str: + settings = get_settings() + expire = datetime.now(timezone.utc) + timedelta(days=VERIFICATION_TOKEN_EXPIRE_DAYS) + payload = {"sub": str(user_id), "exp": expire, "aud": "email_verification"} + return jwt.encode(payload, settings.jwt_secret, algorithm=ALGORITHM) + +def verify_verification_token(token: str) -> Optional[int]: + """Returns user_id if valid token, or None if invalid/expired/wrong purpose""" + settings = get_settings() + try: + payload = jwt.decode(token, settings.jwt_secret, algorithms=[ALGORITHM], audience="email_verification") + user_id = payload.get("sub") + if user_id is None: + return None + return int(user_id) + except JWTError: + return None \ No newline at end of file From 8eef3f81583c5c620c9339190a2205afcef00e4c Mon Sep 17 00:00:00 2001 From: Ethan Date: Sun, 28 Jun 2026 23:29:07 -0700 Subject: [PATCH 03/12] feat(email-verification): add resend python dependency; add resend email service --- backend/app/services/email_service.py | 17 +++++++++++++++++ backend/requirements.txt | 3 +++ 2 files changed, 20 insertions(+) create mode 100644 backend/app/services/email_service.py diff --git a/backend/app/services/email_service.py b/backend/app/services/email_service.py new file mode 100644 index 00000000..28bba273 --- /dev/null +++ b/backend/app/services/email_service.py @@ -0,0 +1,17 @@ +import resend + +from app.core.config import get_settings + + +async def send_verification_email(to: str, token: str) -> None: + settings = get_settings() + resend.api_key = settings.resend_api_key + + params: resend.Emails.SendParams = { + "from": "NEXUS ", + "to": to, + "subject": "Verify Your Email on NEXUS", + "text": f"Please verify your email: {settings.frontend_url.rstrip('/')}/verify-email?token={token}" + } + + resend.Emails.send_async(params) \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index ec35ae12..425b1032 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -28,6 +28,9 @@ google-auth==2.37.0 google-auth-oauthlib==1.2.1 google-api-python-client==2.154.0 +# Resend (email services) API +resend==2.32.2 + # Testing pytest==8.3.4 pytest-asyncio==0.24.0 From cadcbbef173890f0e263d363f7d87a341234f1ef Mon Sep 17 00:00:00 2001 From: Ethan Date: Mon, 29 Jun 2026 00:14:22 -0700 Subject: [PATCH 04/12] feat(email-verification): add routes --- backend/app/api/routes/auth.py | 34 +++++++++++++++++++++++++-- backend/app/api/routes/users.py | 11 ++------- backend/app/core/users.py | 9 ++++++- backend/app/services/email_service.py | 2 +- 4 files changed, 43 insertions(+), 13 deletions(-) diff --git a/backend/app/api/routes/auth.py b/backend/app/api/routes/auth.py index 36470dc3..4d698f33 100644 --- a/backend/app/api/routes/auth.py +++ b/backend/app/api/routes/auth.py @@ -10,11 +10,13 @@ require_admin, ) from app.core.config import get_settings -from app.core.users import check_if_email_exists +from app.core.users import check_if_email_exists, find_user_by_id +from app.core.email_verification import generate_verification_token, verify_verification_token from app.db.session import get_db from app.models.models import User from app.schemas.user import UserResponse from app.schemas.auth import LoginRequest, RegisterRequest, AdminRegisterRequest +from app.services.email_service import send_verification_email router = APIRouter( tags=["auth"]) @@ -72,6 +74,13 @@ def _create_user( db.refresh(user) return user +async def _send_verification_email(to: str, id: int): + try: + await send_verification_email(to, generate_verification_token(id)) + + except Exception: + raise HTTPException(500, "Failed to send verification email.") + @router.post("/auth/login/", response_model=UserResponse) def login(body: LoginRequest, response: Response, db: Session = Depends(get_db)): @@ -138,4 +147,25 @@ def admin_register(body: AdminRegisterRequest, db: Session = Depends(get_db), _: """ check_if_email_exists(db, body.email) - return _create_user(db, body.email, body.first_name, body.last_name, body.role, is_active=False) \ No newline at end of file + return _create_user(db, body.email, body.first_name, body.last_name, body.role, is_active=False) + +@router.get("/auth/verify-email", status_code=status.HTTP_200_OK) +def verify_email(token: str, db: Session = Depends(get_db)): + user_id = verify_verification_token(token) + if user_id is None: + raise HTTPException(400, "Email verification is invalid or has expired.") + + user = find_user_by_id(db, user_id) + user.email_verified = True + db.commit() + return {"detail": "User email successfully verified"} + +# todo: add rate limiting +@router.post("/auth/send-email-verification", status_code=status.HTTP_200_OK) +async def resend_email_verification(user: User = Depends(get_current_user)): + if user.email_verified: + raise HTTPException(400, "User email already verified.") + + await _send_verification_email(user.email, user.id) + + return {"detail": "Verification email successfully sent."} \ No newline at end of file diff --git a/backend/app/api/routes/users.py b/backend/app/api/routes/users.py index 51e01afc..f70c06ff 100644 --- a/backend/app/api/routes/users.py +++ b/backend/app/api/routes/users.py @@ -4,20 +4,13 @@ from app.core.auth import get_current_user, require_admin from app.core.permissions import MANAGE_TOURNAMENT, MANAGE_VOLUNTEERS, has_permission -from app.core.users import check_if_email_exists +from app.core.users import check_if_email_exists, find_user_by_id from app.db.session import get_db from app.models.models import Membership, User from app.schemas.user import UserResponse, UserUpdate, AdminUserUpdate router = APIRouter(tags=["users"]) -def _find_user_by_id(db: Session, id: int) -> User: - user = db.query(User).filter(User.id == id).first() - if not user: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") - return user - - # --------------------------------------------------------------------------- # GET /users/ — admin only (global unscoped list) @@ -74,7 +67,7 @@ def admin_update_user( _: User = Depends(require_admin) ): """Admin can only update a user's role and is_active status.""" - user = _find_user_by_id(db, user_id) + user = find_user_by_id(db, user_id) for field, value in body.model_dump(exclude_unset=True).items(): setattr(user, field, value) db.commit() diff --git a/backend/app/core/users.py b/backend/app/core/users.py index 2c0cd8b5..dc5152f8 100644 --- a/backend/app/core/users.py +++ b/backend/app/core/users.py @@ -9,4 +9,11 @@ def check_if_email_exists(db: Session, email: str): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Email already registered", - ) \ No newline at end of file + ) + + +def find_user_by_id(db: Session, id: int) -> User: + user = db.query(User).filter(User.id == id).first() + if not user: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") + return user \ No newline at end of file diff --git a/backend/app/services/email_service.py b/backend/app/services/email_service.py index 28bba273..16217000 100644 --- a/backend/app/services/email_service.py +++ b/backend/app/services/email_service.py @@ -14,4 +14,4 @@ async def send_verification_email(to: str, token: str) -> None: "text": f"Please verify your email: {settings.frontend_url.rstrip('/')}/verify-email?token={token}" } - resend.Emails.send_async(params) \ No newline at end of file + await resend.Emails.send_async(params) \ No newline at end of file From 3bb8d073c1803fe815994eeb4ce500f21a7f0d2c Mon Sep 17 00:00:00 2001 From: Ethan Date: Mon, 29 Jun 2026 17:07:53 -0700 Subject: [PATCH 05/12] chore(email-verification): update api docs --- backend/app/api/routes/auth.py | 32 ++++++++++++++++++++++++-------- backend/app/schemas/auth.py | 6 +++++- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/backend/app/api/routes/auth.py b/backend/app/api/routes/auth.py index 4d698f33..efdcd329 100644 --- a/backend/app/api/routes/auth.py +++ b/backend/app/api/routes/auth.py @@ -15,7 +15,7 @@ from app.db.session import get_db from app.models.models import User from app.schemas.user import UserResponse -from app.schemas.auth import LoginRequest, RegisterRequest, AdminRegisterRequest +from app.schemas.auth import LoginRequest, RegisterRequest, AdminRegisterRequest, MessageResponse from app.services.email_service import send_verification_email router = APIRouter( tags=["auth"]) @@ -79,7 +79,7 @@ async def _send_verification_email(to: str, id: int): await send_verification_email(to, generate_verification_token(id)) except Exception: - raise HTTPException(500, "Failed to send verification email.") + raise HTTPException(500, "Failed to send verification email") @router.post("/auth/login/", response_model=UserResponse) @@ -149,11 +149,19 @@ def admin_register(body: AdminRegisterRequest, db: Session = Depends(get_db), _: return _create_user(db, body.email, body.first_name, body.last_name, body.role, is_active=False) -@router.get("/auth/verify-email", status_code=status.HTTP_200_OK) +@router.get( + "/auth/verify-email", + status_code=status.HTTP_200_OK, + response_model=MessageResponse, + responses={ + 400: {"description": "Invalid or expired token"}, + 404: {"description": "User not found"}, + }, +) def verify_email(token: str, db: Session = Depends(get_db)): user_id = verify_verification_token(token) if user_id is None: - raise HTTPException(400, "Email verification is invalid or has expired.") + raise HTTPException(400, "Invalid or expired token") user = find_user_by_id(db, user_id) user.email_verified = True @@ -161,11 +169,19 @@ def verify_email(token: str, db: Session = Depends(get_db)): return {"detail": "User email successfully verified"} # todo: add rate limiting -@router.post("/auth/send-email-verification", status_code=status.HTTP_200_OK) -async def resend_email_verification(user: User = Depends(get_current_user)): +@router.post( + "/auth/send-email-verification", + status_code=status.HTTP_200_OK, + response_model=MessageResponse, + responses={ + 400: {"description": "Email already verified"}, + 500: {"description": "Failed to send verification email"}, + }, +) +async def send_email_verification(user: User = Depends(get_current_user)): if user.email_verified: - raise HTTPException(400, "User email already verified.") + raise HTTPException(400, "Email already verified") await _send_verification_email(user.email, user.id) - return {"detail": "Verification email successfully sent."} \ No newline at end of file + return {"detail": "Verification email successfully sent"} \ No newline at end of file diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py index 6130cf3d..0bc54454 100644 --- a/backend/app/schemas/auth.py +++ b/backend/app/schemas/auth.py @@ -75,4 +75,8 @@ class AdminRegisterRequest(BaseModel): # password is excluded because when user logs into their new account they will make one themselves first_name: str last_name: str - role: Literal["admin", "user"] \ No newline at end of file + role: Literal["admin", "user"] + + +class MessageResponse(BaseModel): + detail: str \ No newline at end of file From 9d5a0d25aeb9a3b86b856bd73661db658c734d8c Mon Sep 17 00:00:00 2001 From: Ethan Date: Mon, 29 Jun 2026 17:18:11 -0700 Subject: [PATCH 06/12] feat(email-verification): add frontend api routes --- backend/app/api/routes/auth.py | 10 ++-------- backend/app/schemas/user.py | 1 + frontend/lib/api.ts | 2 ++ 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/backend/app/api/routes/auth.py b/backend/app/api/routes/auth.py index efdcd329..a9e035d9 100644 --- a/backend/app/api/routes/auth.py +++ b/backend/app/api/routes/auth.py @@ -149,10 +149,7 @@ def admin_register(body: AdminRegisterRequest, db: Session = Depends(get_db), _: return _create_user(db, body.email, body.first_name, body.last_name, body.role, is_active=False) -@router.get( - "/auth/verify-email", - status_code=status.HTTP_200_OK, - response_model=MessageResponse, +@router.get("/auth/verify-email/", status_code=status.HTTP_200_OK, response_model=MessageResponse, responses={ 400: {"description": "Invalid or expired token"}, 404: {"description": "User not found"}, @@ -169,10 +166,7 @@ def verify_email(token: str, db: Session = Depends(get_db)): return {"detail": "User email successfully verified"} # todo: add rate limiting -@router.post( - "/auth/send-email-verification", - status_code=status.HTTP_200_OK, - response_model=MessageResponse, +@router.post("/auth/send-email-verification/", status_code=status.HTTP_200_OK, response_model=MessageResponse, responses={ 400: {"description": "Email already verified"}, 500: {"description": "Failed to send verification email"}, diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py index 7e3efd24..a239ae09 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/schemas/user.py @@ -53,6 +53,7 @@ class UserResponse(BaseModel): email: EmailStr phone: Optional[str] = None + email_verified: bool role: ROLE is_active: bool diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts index 7d229bb8..63221d65 100644 --- a/frontend/lib/api.ts +++ b/frontend/lib/api.ts @@ -119,6 +119,8 @@ export const authApi = { logout: () => api.post('/auth/logout/', {}), me: () => api.get('/auth/me/'), register: (body: AuthRegister) => api.post('/auth/register/', body), + verifyEmail: (token: string) => api.get(`/auth/verify-email/?token=${token}`), + sendEmailVerification: () => api.post('/auth/send-email-verification/', {}), } From b04ed8a7e0fae731f2e0144d8739d2d10be5253c Mon Sep 17 00:00:00 2001 From: Ethan Date: Mon, 29 Jun 2026 19:27:13 -0700 Subject: [PATCH 07/12] feat(email-verification): finish email-verification auth page with success and error states; add Spinner component --- frontend/app/(auth)/verify-email/page.tsx | 134 ++++++++++++++++++++++ frontend/components/ui/Spinner.tsx | 26 +++++ frontend/proxy.ts | 9 ++ 3 files changed, 169 insertions(+) create mode 100644 frontend/app/(auth)/verify-email/page.tsx create mode 100644 frontend/components/ui/Spinner.tsx diff --git a/frontend/app/(auth)/verify-email/page.tsx b/frontend/app/(auth)/verify-email/page.tsx new file mode 100644 index 00000000..750e0e94 --- /dev/null +++ b/frontend/app/(auth)/verify-email/page.tsx @@ -0,0 +1,134 @@ +'use client' + +import { Button } from "@/components/ui/Button" +import { IconCheckCircleSolid, IconXCircleSolid } from "@/components/ui/Icons" +import { Spinner } from "@/components/ui/Spinner" +import { ApiError, authApi, User } from "@/lib/api" +import { useRouter, useSearchParams } from "next/navigation" +import { Suspense, useEffect, useState } from "react" + + +export default function VerifyEmailPage() { + return ( + }> + + + ) +} + +function VerifyEmailContent() { + // ── Verify Email states ────────────────────────────────────────────────── + // 1 Loading + // 2 Success + // 3 Error + // ──────────────────────────────────────────────────────────────────────── + const [state, setState] = useState(1) + const [user, setUser] = useState(null) + const [sendEmailSuccess, setSendEmailSuccess] = useState(false) + const [loading, setLoading] = useState(false) + const [errors, setErrors] = useState<{ + verify?: string + send?: string + }>({}) + + const searchParams = useSearchParams() + const token = searchParams.get("token") + + const router = useRouter() + + useEffect(() => { + + authApi.me().then(u => setUser(u)).catch(() => {}) + + authApi.verifyEmail(token ?? '').then(() => setState(2)).catch(err => { + const message = err instanceof ApiError ? err.message : "Something went wrong" + setErrors({verify: message}) + setState(3) + }) + + }, []) + + return ( +
+
+

NEXUS

+
+ +
+

Verify Your Email

+
+ + {state === 1 && ( + + )} + + {state === 2 && ( +
+ + Email successfully verified. + + {user ? ( + + ) : ( + + You can close this window. + + )} +
+ )} + + {state === 3 && ( +
+ + Error: {errors.verify} + + {user ? ( + <> + + {sendEmailSuccess && ( +

+ Email sent successfully. Please check your inbox. +

+ )} + {errors.send && ( +

+ {errors.send} +

+ )} + + ) : ( + + )} +
+ )} +
+ ) +} \ No newline at end of file diff --git a/frontend/components/ui/Spinner.tsx b/frontend/components/ui/Spinner.tsx new file mode 100644 index 00000000..81dfd519 --- /dev/null +++ b/frontend/components/ui/Spinner.tsx @@ -0,0 +1,26 @@ + +type Size = 'sm' | 'md' | 'lg' + +interface SpinnerProps { + size?: Size +} + +const sizeStyles: Record = { + sm: { width: '24px', height: '24px', border: '2px solid rgba(255,255,255,0.4)' }, + md: { width: '48px', height: '48px', border: '3px solid rgba(255,255,255,0.4)' }, + lg: { width: '64px', height: '64px', border: '4px solid rgba(255,255,255,0.4)' } +} + +export function Spinner({ size = 'md' }: SpinnerProps) { + return ( + + + + ) +} diff --git a/frontend/proxy.ts b/frontend/proxy.ts index e03e60c9..4d5338fb 100644 --- a/frontend/proxy.ts +++ b/frontend/proxy.ts @@ -28,6 +28,15 @@ export function proxy(request: NextRequest) { return NextResponse.redirect(url) } + if (pathname === '/verify-email') { + const verifyToken = request.nextUrl.searchParams.get('token') + if (!verifyToken) { + const url = request.nextUrl.clone() + url.pathname = token ? '/dashboard' : '/' + return NextResponse.redirect(url) + } + } + return NextResponse.next() } From 6a3e599852de4f7e0b1b45f06c6f030b921d3a74 Mon Sep 17 00:00:00 2001 From: Ethan Date: Mon, 29 Jun 2026 20:12:56 -0700 Subject: [PATCH 08/12] refactor(sign-up): change magic numbers to STATE constants --- frontend/app/(auth)/sign-up/page.tsx | 156 ++++++++++++++------------- frontend/components/ui/Input.tsx | 2 +- 2 files changed, 84 insertions(+), 74 deletions(-) diff --git a/frontend/app/(auth)/sign-up/page.tsx b/frontend/app/(auth)/sign-up/page.tsx index 806402e2..5b683556 100644 --- a/frontend/app/(auth)/sign-up/page.tsx +++ b/frontend/app/(auth)/sign-up/page.tsx @@ -60,6 +60,20 @@ function clearCookie() { document.cookie = "inSignUpFlow=; path=/; max-age=0" } +const STATE = { + ACCOUNT: 1, + STUDENT_STATUS: 2, + UNIVERSITY: 3, + EMPLOYER: 4, + COMPETED_BEFORE: 5, + COMPETITION_EXP: 6, + VOLUNTEERED_BEFORE: 7, + VOLUNTEERING_EXP: 8, + SHIRT_SIZE: 9, + DIETARY_RESTRICTIONS: 10, + DIETARY_TEXT: 11, + COMPLETE: 12, +} as const export default function SignUpPage() { // ── Sign-up step states ────────────────────────────────────────────────── @@ -76,7 +90,7 @@ export default function SignUpPage() { // 11 Dietary restriction text // 12 Complete button activated // ──────────────────────────────────────────────────────────────────────── - const [state, setState] = useState(1) + const [state, setState] = useState(STATE.ACCOUNT) const [user, setUser] = useState(null) const [loading, setLoading] = useState(false) @@ -144,7 +158,7 @@ export default function SignUpPage() { authApi.me().then(user => { if (document.cookie.includes("inSignUpFlow")) { setUser(user) - setState(2) + setState(STATE.STUDENT_STATUS) } else { router.push('/dashboard') } @@ -182,7 +196,7 @@ export default function SignUpPage() { setCookie() - setState(2) + setState(STATE.STUDENT_STATUS) } catch (error: unknown) { if (error instanceof ApiError) { setErrors({ form1: error.message }) @@ -242,7 +256,7 @@ export default function SignUpPage() { return ( <> - {state === 1 && ( + {state === STATE.ACCOUNT && (
-
+ {errors.form1 && ( -

+

{errors.form1}

)} @@ -378,7 +388,7 @@ export default function SignUpPage() {
)} - {state >= 2 && ( + {state >= STATE.STUDENT_STATUS && (

Complete Your Profile

- + setState(5)} - isActive={state === 2} + onSkip={() => setState(STATE.STUDENT_STATUS + 3)} + isActive={state === STATE.STUDENT_STATUS} > { setProfileData(d => ({...d, university: undefined, major: undefined, year_level: undefined, graduation_year: undefined})) - setState(5) + setState(STATE.UNIVERSITY + 2) }} onNext={() => { const ers: typeof errors = {} @@ -481,9 +491,9 @@ export default function SignUpPage() { else if (profileData.graduation_year < 1000 || profileData.graduation_year > 9999) ers.graduation_year = "Must be a valid year." - Object.keys(ers).length > 0 ? setErrors(er => ({...er, ...ers})) : setState(5) + Object.keys(ers).length > 0 ? setErrors(er => ({...er, ...ers})) : setState(STATE.UNIVERSITY + 2) }} - isActive={state === 3} + isActive={state === STATE.UNIVERSITY} > )} - {state >= 4 && profileData.student_status === "Non-Student" && ( + {state >= STATE.EMPLOYER && profileData.student_status === "Non-Student" && ( setState(5)} + onSkip={() => setState(STATE.COMPETED_BEFORE)} onNext={() => { - !profileData.employer ? setErrors(er => ({...er, employer: "Cannot be empty."})) : setState(5) + !profileData.employer ? setErrors(er => ({...er, employer: "Cannot be empty."})) : setState(STATE.COMPETED_BEFORE) }} - isActive={state === 4} + isActive={state === STATE.EMPLOYER} > )} - {state >= 5 && ( + {state >= STATE.COMPETED_BEFORE && ( { - setState(7) + setState(STATE.COMPETED_BEFORE + 2) }} - isActive={state === 5} + isActive={state === STATE.COMPETED_BEFORE} >
- { setCompetedBefore(true) - if (state >= 7) return - setState(6) + if (state >= STATE.COMPETED_BEFORE + 2) return + setState(STATE.COMPETITION_EXP) }} label="Yes" showCircle={false} solid /> - { + onChange={() => { setCompetedBefore(false) - if (state >= 7) return - setState(7) + if (state >= STATE.COMPETED_BEFORE + 2) return + setState(STATE.COMPETED_BEFORE + 2) }} label="No" showCircle={false} @@ -557,18 +567,18 @@ export default function SignUpPage() { )} - {state >= 6 && competedBefore && ( + {state >= STATE.COMPETITION_EXP && competedBefore && ( { setCompetedBefore(null) - setState(7) + setState(STATE.VOLUNTEERED_BEFORE) }} onNext={() => { !profileData.competition_exp ? setErrors(er => ({...er, competition_exp: "Cannot be empty."})) - : setState(7) + : setState(STATE.VOLUNTEERED_BEFORE) }} - isActive={state === 6} + isActive={state === STATE.COMPETITION_EXP} >