diff --git a/backend/app/interfaces/volunteer_data_service.py b/backend/app/interfaces/volunteer_data_service.py new file mode 100644 index 00000000..ec0b293d --- /dev/null +++ b/backend/app/interfaces/volunteer_data_service.py @@ -0,0 +1,51 @@ +""" +Interface for volunteer data service operations. +Defines the contract for volunteer data CRUD operations. +""" + +from abc import ABC, abstractmethod +from typing import List + +from app.schemas.volunteer_data import ( + VolunteerDataCreateRequest, + VolunteerDataResponse, + VolunteerDataUpdateRequest, +) + + +class IVolunteerDataService(ABC): + """ + Interface for volunteer data service operations + """ + + @abstractmethod + async def create_volunteer_data(self, volunteer_data: VolunteerDataCreateRequest) -> VolunteerDataResponse: + """Create new volunteer data entry""" + pass + + @abstractmethod + async def get_volunteer_data_by_id(self, volunteer_data_id: str) -> VolunteerDataResponse: + """Get volunteer data by ID""" + pass + + @abstractmethod + async def get_volunteer_data_by_user_id(self, user_id: str) -> VolunteerDataResponse: + """Get volunteer data by user ID""" + pass + + @abstractmethod + async def get_all_volunteer_data(self) -> List[VolunteerDataResponse]: + """Get all volunteer data entries""" + pass + + @abstractmethod + async def update_volunteer_data_by_id( + self, volunteer_data_id: str, volunteer_data_update: VolunteerDataUpdateRequest + ) -> VolunteerDataResponse: + """Update volunteer data by ID""" + pass + + @abstractmethod + async def delete_volunteer_data_by_id(self, volunteer_data_id: str) -> None: + """Delete volunteer data by ID""" + pass diff --git a/backend/app/models/User.py b/backend/app/models/User.py index e605edd7..1a57d8ce 100644 --- a/backend/app/models/User.py +++ b/backend/app/models/User.py @@ -49,3 +49,5 @@ class User(Base): participant_matches = relationship("Match", back_populates="participant", foreign_keys=[Match.participant_id]) volunteer_matches = relationship("Match", back_populates="volunteer", foreign_keys=[Match.volunteer_id]) + + volunteer_data = relationship("VolunteerData", back_populates="user", uselist=False) diff --git a/backend/app/models/VolunteerData.py b/backend/app/models/VolunteerData.py new file mode 100644 index 00000000..ee548f92 --- /dev/null +++ b/backend/app/models/VolunteerData.py @@ -0,0 +1,21 @@ +import uuid +from datetime import datetime + +from sqlalchemy import Column, DateTime, ForeignKey, Text +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import relationship + +from .Base import Base + + +class VolunteerData(Base): + __tablename__ = "volunteer_data" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) + experience = Column(Text, nullable=True) + references_json = Column(Text, nullable=True) + additional_comments = Column(Text, nullable=True) + submitted_at = Column(DateTime, default=datetime.utcnow, nullable=False) + + user = relationship("User", back_populates="volunteer_data") diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index bc196d7d..5df2612b 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -24,6 +24,7 @@ from .Treatment import Treatment from .User import FormStatus, User from .UserData import UserData +from .VolunteerData import VolunteerData # Used to avoid import errors for the models __all__ = [ @@ -48,6 +49,7 @@ "TaskType", "TaskPriority", "TaskStatus", + "VolunteerData", ] log = logging.getLogger(LOGGER_NAME("models")) diff --git a/backend/app/routes/volunteer_data.py b/backend/app/routes/volunteer_data.py new file mode 100644 index 00000000..8a11961a --- /dev/null +++ b/backend/app/routes/volunteer_data.py @@ -0,0 +1,131 @@ +from fastapi import APIRouter, Depends, HTTPException + +from app.middleware.auth import has_roles +from app.schemas.user import UserRole +from app.schemas.volunteer_data import ( + VolunteerDataCreateRequest, + VolunteerDataListResponse, + VolunteerDataPublicSubmission, + VolunteerDataResponse, + VolunteerDataUpdateRequest, +) +from app.services.implementations.volunteer_data_service import VolunteerDataService +from app.utilities.service_utils import get_volunteer_data_service + +router = APIRouter( + prefix="/volunteer-data", + tags=["volunteer-data"], +) + + +# Public endpoint - anyone can submit volunteer data +@router.post("/submit", response_model=VolunteerDataResponse) +async def submit_volunteer_data( + volunteer_data: VolunteerDataPublicSubmission, + volunteer_data_service: VolunteerDataService = Depends(get_volunteer_data_service), +): + """Public endpoint for volunteers to submit their application data""" + try: + create_request = VolunteerDataCreateRequest( + user_id=None, + experience=volunteer_data.experience, + references_json=volunteer_data.references_json, + additional_comments=volunteer_data.additional_comments, + ) + return await volunteer_data_service.create_volunteer_data(create_request) + except HTTPException as http_ex: + raise http_ex + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +# Admin only - create volunteer data +@router.post("/", response_model=VolunteerDataResponse) +async def create_volunteer_data( + volunteer_data: VolunteerDataCreateRequest, + volunteer_data_service: VolunteerDataService = Depends(get_volunteer_data_service), + authorized: bool = has_roles([UserRole.ADMIN]), +): + try: + return await volunteer_data_service.create_volunteer_data(volunteer_data) + except HTTPException as http_ex: + raise http_ex + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +# Admin only - get all volunteer data +@router.get("/", response_model=VolunteerDataListResponse) +async def get_all_volunteer_data( + volunteer_data_service: VolunteerDataService = Depends(get_volunteer_data_service), + authorized: bool = has_roles([UserRole.ADMIN]), +): + try: + volunteer_data_list = await volunteer_data_service.get_all_volunteer_data() + return VolunteerDataListResponse(volunteer_data=volunteer_data_list, total=len(volunteer_data_list)) + except HTTPException as http_ex: + raise http_ex + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +# Admin only - get volunteer data by ID +@router.get("/{volunteer_data_id}", response_model=VolunteerDataResponse) +async def get_volunteer_data( + volunteer_data_id: str, + volunteer_data_service: VolunteerDataService = Depends(get_volunteer_data_service), + authorized: bool = has_roles([UserRole.ADMIN]), +): + try: + return await volunteer_data_service.get_volunteer_data_by_id(volunteer_data_id) + except HTTPException as http_ex: + raise http_ex + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +# Admin only - get volunteer data by user ID +@router.get("/user/{user_id}", response_model=VolunteerDataResponse) +async def get_volunteer_data_by_user( + user_id: str, + volunteer_data_service: VolunteerDataService = Depends(get_volunteer_data_service), + authorized: bool = has_roles([UserRole.ADMIN]), +): + try: + return await volunteer_data_service.get_volunteer_data_by_user_id(user_id) + except HTTPException as http_ex: + raise http_ex + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +# Admin only - update volunteer data +@router.put("/{volunteer_data_id}", response_model=VolunteerDataResponse) +async def update_volunteer_data( + volunteer_data_id: str, + volunteer_data_update: VolunteerDataUpdateRequest, + volunteer_data_service: VolunteerDataService = Depends(get_volunteer_data_service), + authorized: bool = has_roles([UserRole.ADMIN]), +): + try: + return await volunteer_data_service.update_volunteer_data_by_id(volunteer_data_id, volunteer_data_update) + except HTTPException as http_ex: + raise http_ex + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +# Admin only - delete volunteer data +@router.delete("/{volunteer_data_id}") +async def delete_volunteer_data( + volunteer_data_id: str, + volunteer_data_service: VolunteerDataService = Depends(get_volunteer_data_service), + authorized: bool = has_roles([UserRole.ADMIN]), +): + try: + await volunteer_data_service.delete_volunteer_data_by_id(volunteer_data_id) + return {"message": "Volunteer data deleted successfully"} + except HTTPException as http_ex: + raise http_ex + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/app/schemas/volunteer_data.py b/backend/app/schemas/volunteer_data.py new file mode 100644 index 00000000..4b7eaea8 --- /dev/null +++ b/backend/app/schemas/volunteer_data.py @@ -0,0 +1,72 @@ +""" +Pydantic schemas for volunteer data validation and serialization. +Handles volunteer data CRUD and response models for the API. +""" + +from datetime import datetime +from typing import List, Optional +from uuid import UUID + +from pydantic import BaseModel, ConfigDict, Field + + +class VolunteerDataBase(BaseModel): + """ + Base schema for volunteer data model with common attributes. + """ + + experience: Optional[str] = Field(None, description="Volunteer experience description") + references_json: Optional[str] = Field(None, description="JSON string containing references") + additional_comments: Optional[str] = Field(None, description="Additional comments about volunteering") + + +class VolunteerDataCreateRequest(VolunteerDataBase): + """ + Request schema for creating volunteer data + """ + + user_id: Optional[UUID] = Field( + None, description="User ID this volunteer data belongs to (optional for public submissions)" + ) + + +class VolunteerDataPublicSubmission(VolunteerDataBase): + """ + Request schema for public volunteer data submissions (no user_id required) + """ + + pass + + +class VolunteerDataUpdateRequest(BaseModel): + """ + Request schema for updating volunteer data, all fields optional + """ + + experience: Optional[str] = Field(None, description="Volunteer experience description") + references_json: Optional[str] = Field(None, description="JSON string containing references") + additional_comments: Optional[str] = Field(None, description="Additional comments about volunteering") + + +class VolunteerDataResponse(BaseModel): + """ + Response schema for volunteer data + """ + + id: UUID + user_id: Optional[UUID] + experience: Optional[str] + references_json: Optional[str] + additional_comments: Optional[str] + submitted_at: datetime + + model_config = ConfigDict(from_attributes=True) + + +class VolunteerDataListResponse(BaseModel): + """ + Response schema for listing volunteer data + """ + + volunteer_data: List[VolunteerDataResponse] + total: int diff --git a/backend/app/server.py b/backend/app/server.py index ed62fd45..24ad54dc 100644 --- a/backend/app/server.py +++ b/backend/app/server.py @@ -8,7 +8,20 @@ from . import models from .middleware.auth_middleware import AuthMiddleware -from .routes import auth, availability, intake, match, matching, ranking, send_email, suggested_times, task, test, user +from .routes import ( + auth, + availability, + intake, + match, + matching, + ranking, + send_email, + suggested_times, + task, + test, + user, + volunteer_data, +) from .utilities.constants import LOGGER_NAME from .utilities.firebase_init import initialize_firebase from .utilities.ses.ses_init import ensure_ses_templates @@ -71,6 +84,7 @@ async def lifespan(_: FastAPI): app.include_router(matching.router) app.include_router(intake.router) app.include_router(ranking.router) +app.include_router(volunteer_data.router) app.include_router(send_email.router) app.include_router(task.router) app.include_router(test.router) diff --git a/backend/app/services/implementations/volunteer_data_service.py b/backend/app/services/implementations/volunteer_data_service.py new file mode 100644 index 00000000..41542220 --- /dev/null +++ b/backend/app/services/implementations/volunteer_data_service.py @@ -0,0 +1,134 @@ +import logging +from typing import List +from uuid import UUID + +from fastapi import HTTPException +from sqlalchemy.orm import Session + +from app.interfaces.volunteer_data_service import IVolunteerDataService +from app.models.VolunteerData import VolunteerData +from app.schemas.volunteer_data import ( + VolunteerDataCreateRequest, + VolunteerDataResponse, + VolunteerDataUpdateRequest, +) +from app.utilities.constants import LOGGER_NAME + + +class VolunteerDataService(IVolunteerDataService): + def __init__(self, db: Session): + self.db = db + self.logger = logging.getLogger(LOGGER_NAME("volunteer_data_service")) + + async def create_volunteer_data(self, volunteer_data: VolunteerDataCreateRequest) -> VolunteerDataResponse: + try: + if volunteer_data.user_id is not None: + existing_data = ( + self.db.query(VolunteerData).filter(VolunteerData.user_id == volunteer_data.user_id).first() + ) + if existing_data: + raise HTTPException(status_code=409, detail="Volunteer data already exists for this user") + + # Create new volunteer data entry + db_volunteer_data = VolunteerData( + user_id=volunteer_data.user_id, + experience=volunteer_data.experience, + references_json=volunteer_data.references_json, + additional_comments=volunteer_data.additional_comments, + ) + + self.db.add(db_volunteer_data) + self.db.commit() + self.db.refresh(db_volunteer_data) + + return VolunteerDataResponse.model_validate(db_volunteer_data) + + except HTTPException: + raise + except Exception as e: + self.db.rollback() + self.logger.error(f"Error creating volunteer data: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + async def get_volunteer_data_by_id(self, volunteer_data_id: str) -> VolunteerDataResponse: + try: + volunteer_data = self.db.query(VolunteerData).filter(VolunteerData.id == UUID(volunteer_data_id)).first() + if not volunteer_data: + raise HTTPException(status_code=404, detail="Volunteer data not found") + + return VolunteerDataResponse.model_validate(volunteer_data) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid volunteer data ID format") + except HTTPException: + raise + except Exception as e: + self.logger.error(f"Error retrieving volunteer data {volunteer_data_id}: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + async def get_volunteer_data_by_user_id(self, user_id: str) -> VolunteerDataResponse: + try: + volunteer_data = self.db.query(VolunteerData).filter(VolunteerData.user_id == UUID(user_id)).first() + if not volunteer_data: + raise HTTPException(status_code=404, detail="Volunteer data not found for this user") + + return VolunteerDataResponse.model_validate(volunteer_data) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid user ID format") + except HTTPException: + raise + except Exception as e: + self.logger.error(f"Error retrieving volunteer data for user {user_id}: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + async def get_all_volunteer_data(self) -> List[VolunteerDataResponse]: + try: + volunteer_data_list = self.db.query(VolunteerData).all() + return [VolunteerDataResponse.model_validate(data) for data in volunteer_data_list] + except Exception as e: + self.logger.error(f"Error getting all volunteer data: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + async def update_volunteer_data_by_id( + self, volunteer_data_id: str, volunteer_data_update: VolunteerDataUpdateRequest + ) -> VolunteerDataResponse: + try: + db_volunteer_data = self.db.query(VolunteerData).filter(VolunteerData.id == UUID(volunteer_data_id)).first() + if not db_volunteer_data: + raise HTTPException(status_code=404, detail="Volunteer data not found") + + # Update provided fields only + update_data = volunteer_data_update.model_dump(exclude_unset=True) + + for field, value in update_data.items(): + setattr(db_volunteer_data, field, value) + + self.db.commit() + self.db.refresh(db_volunteer_data) + + return VolunteerDataResponse.model_validate(db_volunteer_data) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid volunteer data ID format") + except HTTPException: + raise + except Exception as e: + self.db.rollback() + self.logger.error(f"Error updating volunteer data {volunteer_data_id}: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + async def delete_volunteer_data_by_id(self, volunteer_data_id: str) -> None: + try: + db_volunteer_data = self.db.query(VolunteerData).filter(VolunteerData.id == UUID(volunteer_data_id)).first() + if not db_volunteer_data: + raise HTTPException(status_code=404, detail="Volunteer data not found") + + self.db.delete(db_volunteer_data) + self.db.commit() + + except ValueError: + raise HTTPException(status_code=400, detail="Invalid volunteer data ID format") + except HTTPException: + raise + except Exception as e: + self.db.rollback() + self.logger.error(f"Error deleting volunteer data {volunteer_data_id}: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/app/utilities/service_utils.py b/backend/app/utilities/service_utils.py index 23c8dc08..3a8766a6 100644 --- a/backend/app/utilities/service_utils.py +++ b/backend/app/utilities/service_utils.py @@ -6,6 +6,7 @@ from ..services.implementations.auth_service import AuthService from ..services.implementations.task_service import TaskService from ..services.implementations.user_service import UserService +from ..services.implementations.volunteer_data_service import VolunteerDataService from .db_utils import get_db @@ -13,6 +14,10 @@ def get_user_service(db: Session = Depends(get_db)): return UserService(db) +def get_volunteer_data_service(db: Session = Depends(get_db)): + return VolunteerDataService(db) + + def get_auth_service(user_service: UserService = Depends(get_user_service)): logger = logging.getLogger(__name__) return AuthService(logger=logger, user_service=user_service) diff --git a/backend/migrations/versions/9f1a6d727929_add_volunteer_data_table_with_nullable_user_id.py b/backend/migrations/versions/9f1a6d727929_add_volunteer_data_table_with_nullable_user_id.py new file mode 100644 index 00000000..a71800c8 --- /dev/null +++ b/backend/migrations/versions/9f1a6d727929_add_volunteer_data_table_with_nullable_user_id.py @@ -0,0 +1,38 @@ +"""add_volunteer_data_table_with_nullable_user_id + +Revision ID: 9f1a6d727929 +Revises: ba215810568b +Create Date: 2025-10-09 20:00:20.836685 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "9f1a6d727929" +down_revision: Union[str, None] = "ba215810568b" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "volunteer_data", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("user_id", sa.UUID(), nullable=True), + sa.Column("experience", sa.Text(), nullable=True), + sa.Column("references_json", sa.Text(), nullable=True), + sa.Column("additional_comments", sa.Text(), nullable=True), + sa.Column("submitted_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + + +def downgrade() -> None: + op.drop_table("volunteer_data") diff --git a/frontend/src/pages/api/volunteer-data.ts b/frontend/src/pages/api/volunteer-data.ts new file mode 100644 index 00000000..42cc4444 --- /dev/null +++ b/frontend/src/pages/api/volunteer-data.ts @@ -0,0 +1,74 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { method } = req; + + try { + let url = `${BACKEND_URL}/volunteer-data`; + let fetchOptions: RequestInit = { + method, + headers: { + 'Content-Type': 'application/json', + // Forward authorization header if present + ...(req.headers.authorization && { + Authorization: req.headers.authorization, + }), + }, + }; + + // Handle different HTTP methods + switch (method) { + case 'POST': + // Create volunteer data + fetchOptions.body = JSON.stringify(req.body); + break; + + case 'GET': + // Get volunteer data - handle query parameters + if (req.query.id) { + url = `${BACKEND_URL}/volunteer-data/${req.query.id}`; + } else if (req.query.user_id) { + url = `${BACKEND_URL}/volunteer-data/user/${req.query.user_id}`; + } + // If no specific query, it will get all volunteer data + break; + + case 'PUT': + // Update volunteer data + if (req.query.id) { + url = `${BACKEND_URL}/volunteer-data/${req.query.id}`; + fetchOptions.body = JSON.stringify(req.body); + } else { + return res.status(400).json({ error: 'ID required for PUT request' }); + } + break; + + case 'DELETE': + // Delete volunteer data + if (req.query.id) { + url = `${BACKEND_URL}/volunteer-data/${req.query.id}`; + } else { + return res.status(400).json({ error: 'ID required for DELETE request' }); + } + break; + + default: + return res.status(405).json({ error: `Method ${method} not allowed` }); + } + + // Make request to FastAPI backend + const response = await fetch(url, fetchOptions); + const data = await response.json(); + + // Forward the response status and data + res.status(response.status).json(data); + } catch (error) { + console.error('API proxy error:', error); + res.status(500).json({ + error: 'Internal server error', + details: error instanceof Error ? error.message : 'Unknown error', + }); + } +} diff --git a/frontend/src/pages/api/volunteer-data/submit.ts b/frontend/src/pages/api/volunteer-data/submit.ts new file mode 100644 index 00000000..7a94c808 --- /dev/null +++ b/frontend/src/pages/api/volunteer-data/submit.ts @@ -0,0 +1,28 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const response = await fetch(`${BACKEND_URL}/volunteer-data/submit`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(req.body), + }); + + const data = await response.json(); + res.status(response.status).json(data); + } catch (error) { + console.error('API proxy error:', error); + res.status(500).json({ + error: 'Internal server error', + details: error instanceof Error ? error.message : 'Unknown error', + }); + } +} diff --git a/frontend/src/pages/volunteer/secondary.tsx b/frontend/src/pages/volunteer/secondary.tsx new file mode 100644 index 00000000..76633925 --- /dev/null +++ b/frontend/src/pages/volunteer/secondary.tsx @@ -0,0 +1,403 @@ +import React, { useState } from 'react'; +import { ChevronRightIcon, CheckCircleIcon, UserIcon } from '@heroicons/react/24/outline'; + +interface Reference { + name: string; + email: string; + phone: string; +} + +export default function VolunteerSecondary() { + const [currentStep, setCurrentStep] = useState(0); + const [experience, setExperience] = useState(''); + const [references, setReferences] = useState([ + { name: '', email: '', phone: '' }, + { name: '', email: '', phone: '' }, + ]); + const [additionalComments, setAdditionalComments] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + + const wordCount = + experience.trim() === '' + ? 0 + : experience + .trim() + .split(/\s+/) + .filter((word) => word.length > 0).length; + const MAX_WORDS = 300; + + const handleReferenceChange = (index: number, field: keyof Reference, value: string) => { + const newReferences = [...references]; + newReferences[index][field] = value; + setReferences(newReferences); + }; + + const handleInputFocus = (e: React.FocusEvent) => { + e.target.style.borderColor = '#056067'; + e.target.style.boxShadow = `0 0 0 2px rgba(5, 96, 103, 0.2)`; + }; + + const handleInputBlur = (e: React.FocusEvent) => { + e.target.style.borderColor = 'rgb(209 213 219)'; + e.target.style.boxShadow = 'none'; + }; + + const handleSubmit = async () => { + setIsSubmitting(true); + setError(null); + + try { + const volunteerData = { + experience, + references_json: JSON.stringify(references), + additional_comments: additionalComments, + }; + + const response = await fetch('/api/volunteer-data/submit', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(volunteerData), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.detail || 'Failed to submit volunteer data'); + } + + const result = await response.json(); + console.log('Volunteer data submitted successfully:', result); + setCurrentStep(4); // Go to success page + } catch (err) { + console.error('Error submitting volunteer data:', err); + setError(err instanceof Error ? err.message : 'Failed to submit data'); + } finally { + setIsSubmitting(false); + } + }; + + // Step 0: Setup Introduction + if (currentStep === 0) { + return ( +
+
+
+ + {/* Checkmark overlay */} +
+ + + +
+
+ +

+ Let's setup your public volunteer profile +

+ +

+ Your experience provided in this form will +
+ be shared with potential matches. +

+ + +
+
+ ); + } + + // Step 1: Experience Form + if (currentStep === 1) { + return ( +
+
+

Volunteer Profile Form

+ + {/* Progress Bar */} +
+
+
+
+
+
+ +
+
+

Your Experience

+

+ This information will serve as your biography to be shared with potential matches. +

+
+ +
+ +