From be5e4bf5fcdb369c6951f1b9ddc8be8259e8e7db Mon Sep 17 00:00:00 2001 From: matia-lee Date: Thu, 9 Oct 2025 19:29:58 -0400 Subject: [PATCH 1/5] backend from before with resolved merge conflicts? --- .../app/interfaces/volunteer_data_service.py | 53 +++ backend/app/models/User.py | 2 + backend/app/models/VolunteerData.py | 21 + backend/app/models/__init__.py | 2 + backend/app/routes/volunteer_data.py | 135 ++++++ backend/app/schemas/volunteer_data.py | 73 ++++ backend/app/server.py | 3 +- .../implementations/volunteer_data_service.py | 157 +++++++ backend/app/utilities/service_utils.py | 4 +- .../6cfd431cb2e9_add_volunteer_data_table.py | 39 ++ ...make_user_id_nullable_in_volunteer_data.py | 22 + frontend/src/pages/api/volunteer-data.ts | 75 ++++ .../src/pages/api/volunteer-data/submit.ts | 29 ++ frontend/src/pages/volunteer/secondary.tsx | 396 ++++++++++++++++++ 14 files changed, 1009 insertions(+), 2 deletions(-) create mode 100644 backend/app/interfaces/volunteer_data_service.py create mode 100644 backend/app/models/VolunteerData.py create mode 100644 backend/app/routes/volunteer_data.py create mode 100644 backend/app/schemas/volunteer_data.py create mode 100644 backend/app/services/implementations/volunteer_data_service.py create mode 100644 backend/migrations/versions/6cfd431cb2e9_add_volunteer_data_table.py create mode 100644 backend/migrations/versions/e71f29bbfe31_make_user_id_nullable_in_volunteer_data.py create mode 100644 frontend/src/pages/api/volunteer-data.ts create mode 100644 frontend/src/pages/api/volunteer-data/submit.ts create mode 100644 frontend/src/pages/volunteer/secondary.tsx diff --git a/backend/app/interfaces/volunteer_data_service.py b/backend/app/interfaces/volunteer_data_service.py new file mode 100644 index 00000000..0a441395 --- /dev/null +++ b/backend/app/interfaces/volunteer_data_service.py @@ -0,0 +1,53 @@ +""" +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..145d7603 --- /dev/null +++ b/backend/app/routes/volunteer_data.py @@ -0,0 +1,135 @@ +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..45da38cb --- /dev/null +++ b/backend/app/schemas/volunteer_data.py @@ -0,0 +1,73 @@ +""" +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: 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..5ec98f2a 100644 --- a/backend/app/server.py +++ b/backend/app/server.py @@ -8,7 +8,7 @@ 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 +71,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..757a88a5 --- /dev/null +++ b/backend/app/services/implementations/volunteer_data_service.py @@ -0,0 +1,157 @@ +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: + # Check if volunteer data already exists for this user + 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..f1a45c7d 100644 --- a/backend/app/utilities/service_utils.py +++ b/backend/app/utilities/service_utils.py @@ -6,17 +6,19 @@ 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 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) - def get_task_service(db: Session = Depends(get_db)): return TaskService(db) diff --git a/backend/migrations/versions/6cfd431cb2e9_add_volunteer_data_table.py b/backend/migrations/versions/6cfd431cb2e9_add_volunteer_data_table.py new file mode 100644 index 00000000..b6d4cb23 --- /dev/null +++ b/backend/migrations/versions/6cfd431cb2e9_add_volunteer_data_table.py @@ -0,0 +1,39 @@ +"""Add volunteer_data table + +Revision ID: 6cfd431cb2e9 +Revises: fef3717e0fc2 +Create Date: 2025-06-21 23:49:32.409851 + +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = '6cfd431cb2e9' +down_revision: Union[str, None] = 'fef3717e0fc2' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('volunteer_data', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('user_id', sa.UUID(), nullable=False), + 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'), + sa.UniqueConstraint('user_id', name='uq_volunteer_data_user_id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('volunteer_data') + # ### end Alembic commands ### diff --git a/backend/migrations/versions/e71f29bbfe31_make_user_id_nullable_in_volunteer_data.py b/backend/migrations/versions/e71f29bbfe31_make_user_id_nullable_in_volunteer_data.py new file mode 100644 index 00000000..46ad59ee --- /dev/null +++ b/backend/migrations/versions/e71f29bbfe31_make_user_id_nullable_in_volunteer_data.py @@ -0,0 +1,22 @@ +"""make_user_id_nullable_in_volunteer_data + +Revision ID: e71f29bbfe31 +Revises: 6cfd431cb2e9 +Create Date: 2025-06-22 01:09:44.851319 + +""" +from typing import Sequence, Union + +# revision identifiers, used by Alembic. +revision: str = 'e71f29bbfe31' +down_revision: Union[str, None] = '6cfd431cb2e9' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + pass + + +def downgrade() -> None: + pass diff --git a/frontend/src/pages/api/volunteer-data.ts b/frontend/src/pages/api/volunteer-data.ts new file mode 100644 index 00000000..bf12e5ce --- /dev/null +++ b/frontend/src/pages/api/volunteer-data.ts @@ -0,0 +1,75 @@ +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' + }); + } +} \ No newline at end of file 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..05ac97ba --- /dev/null +++ b/frontend/src/pages/api/volunteer-data/submit.ts @@ -0,0 +1,29 @@ +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' + }); + } +} \ No newline at end of file diff --git a/frontend/src/pages/volunteer/secondary.tsx b/frontend/src/pages/volunteer/secondary.tsx new file mode 100644 index 00000000..3494f1d7 --- /dev/null +++ b/frontend/src/pages/volunteer/secondary.tsx @@ -0,0 +1,396 @@ +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. +

+
+ +
+ +