diff --git a/backend/alembic/versions/004_add_events.py b/backend/alembic/versions/004_add_events.py new file mode 100644 index 0000000..7ee60ad --- /dev/null +++ b/backend/alembic/versions/004_add_events.py @@ -0,0 +1,60 @@ +"""Add events table and auto_events_enabled to marathons + +Revision ID: 004_add_events +Revises: 003_create_admin +Create Date: 2024-12-14 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = '004_add_events' +down_revision: Union[str, None] = '003_create_admin' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Create events table if it doesn't exist + conn = op.get_bind() + inspector = sa.inspect(conn) + tables = inspector.get_table_names() + + if 'events' not in tables: + op.create_table( + 'events', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('marathon_id', sa.Integer(), nullable=False), + sa.Column('type', sa.String(30), nullable=False), + sa.Column('start_time', sa.DateTime(), nullable=False), + sa.Column('end_time', sa.DateTime(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'), + sa.Column('created_by_id', sa.Integer(), nullable=True), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.func.now()), + sa.PrimaryKeyConstraint('id'), + sa.ForeignKeyConstraint(['marathon_id'], ['marathons.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['created_by_id'], ['users.id'], ondelete='SET NULL'), + ) + + # Create index if it doesn't exist + indexes = [idx['name'] for idx in inspector.get_indexes('events')] + if 'ix_events_marathon_id' not in indexes: + op.create_index('ix_events_marathon_id', 'events', ['marathon_id']) + + # Add auto_events_enabled to marathons if it doesn't exist + columns = [col['name'] for col in inspector.get_columns('marathons')] + if 'auto_events_enabled' not in columns: + op.add_column( + 'marathons', + sa.Column('auto_events_enabled', sa.Boolean(), nullable=False, server_default='true') + ) + + +def downgrade() -> None: + op.drop_column('marathons', 'auto_events_enabled') + op.drop_index('ix_events_marathon_id', table_name='events') + op.drop_table('events') diff --git a/backend/alembic/versions/005_add_assignment_event_type.py b/backend/alembic/versions/005_add_assignment_event_type.py new file mode 100644 index 0000000..918276a --- /dev/null +++ b/backend/alembic/versions/005_add_assignment_event_type.py @@ -0,0 +1,34 @@ +"""Add event_type to assignments + +Revision ID: 005_add_assignment_event_type +Revises: 004_add_events +Create Date: 2024-12-14 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = '005_add_assignment_event_type' +down_revision: Union[str, None] = '004_add_events' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Add event_type column to assignments if it doesn't exist + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = [col['name'] for col in inspector.get_columns('assignments')] + + if 'event_type' not in columns: + op.add_column( + 'assignments', + sa.Column('event_type', sa.String(30), nullable=True) + ) + + +def downgrade() -> None: + op.drop_column('assignments', 'event_type') diff --git a/backend/alembic/versions/006_add_swap_requests.py b/backend/alembic/versions/006_add_swap_requests.py new file mode 100644 index 0000000..32a706f --- /dev/null +++ b/backend/alembic/versions/006_add_swap_requests.py @@ -0,0 +1,54 @@ +"""Add swap_requests table for two-sided swap confirmation + +Revision ID: 006_add_swap_requests +Revises: 005_assignment_event +Create Date: 2024-12-15 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = '006_add_swap_requests' +down_revision: Union[str, None] = '005_add_assignment_event_type' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Create swap_requests table if it doesn't exist + conn = op.get_bind() + inspector = sa.inspect(conn) + tables = inspector.get_table_names() + + if 'swap_requests' not in tables: + op.create_table( + 'swap_requests', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('event_id', sa.Integer(), nullable=False), + sa.Column('from_participant_id', sa.Integer(), nullable=False), + sa.Column('to_participant_id', sa.Integer(), nullable=False), + sa.Column('from_assignment_id', sa.Integer(), nullable=False), + sa.Column('to_assignment_id', sa.Integer(), nullable=False), + sa.Column('status', sa.String(20), nullable=False, server_default='pending'), + sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.func.now()), + sa.Column('responded_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.ForeignKeyConstraint(['event_id'], ['events.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['from_participant_id'], ['participants.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['to_participant_id'], ['participants.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['from_assignment_id'], ['assignments.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['to_assignment_id'], ['assignments.id'], ondelete='CASCADE'), + ) + op.create_index('ix_swap_requests_event_id', 'swap_requests', ['event_id']) + op.create_index('ix_swap_requests_from_participant_id', 'swap_requests', ['from_participant_id']) + op.create_index('ix_swap_requests_to_participant_id', 'swap_requests', ['to_participant_id']) + + +def downgrade() -> None: + op.drop_index('ix_swap_requests_to_participant_id', table_name='swap_requests') + op.drop_index('ix_swap_requests_from_participant_id', table_name='swap_requests') + op.drop_index('ix_swap_requests_event_id', table_name='swap_requests') + op.drop_table('swap_requests') diff --git a/backend/app/api/v1/__init__.py b/backend/app/api/v1/__init__.py index 56f7beb..10ccbe7 100644 --- a/backend/app/api/v1/__init__.py +++ b/backend/app/api/v1/__init__.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from app.api.v1 import auth, users, marathons, games, challenges, wheel, feed, admin +from app.api.v1 import auth, users, marathons, games, challenges, wheel, feed, admin, events router = APIRouter(prefix="/api/v1") @@ -12,3 +12,4 @@ router.include_router(challenges.router) router.include_router(wheel.router) router.include_router(feed.router) router.include_router(admin.router) +router.include_router(events.router) diff --git a/backend/app/api/v1/challenges.py b/backend/app/api/v1/challenges.py index 9e98cba..256ae3c 100644 --- a/backend/app/api/v1/challenges.py +++ b/backend/app/api/v1/challenges.py @@ -81,6 +81,52 @@ async def list_challenges(game_id: int, current_user: CurrentUser, db: DbSession ] +@router.get("/marathons/{marathon_id}/challenges", response_model=list[ChallengeResponse]) +async def list_marathon_challenges(marathon_id: int, current_user: CurrentUser, db: DbSession): + """List all challenges for a marathon (from all approved games). Participants only.""" + # Check marathon exists + result = await db.execute(select(Marathon).where(Marathon.id == marathon_id)) + marathon = result.scalar_one_or_none() + if not marathon: + raise HTTPException(status_code=404, detail="Marathon not found") + + # Check user is participant or admin + participant = await get_participant(db, current_user.id, marathon_id) + if not current_user.is_admin and not participant: + raise HTTPException(status_code=403, detail="You are not a participant of this marathon") + + # Get all challenges from approved games in this marathon + result = await db.execute( + select(Challenge) + .join(Game, Challenge.game_id == Game.id) + .options(selectinload(Challenge.game)) + .where( + Game.marathon_id == marathon_id, + Game.status == GameStatus.APPROVED.value, + ) + .order_by(Game.title, Challenge.difficulty, Challenge.created_at) + ) + challenges = result.scalars().all() + + return [ + ChallengeResponse( + id=c.id, + title=c.title, + description=c.description, + type=c.type, + difficulty=c.difficulty, + points=c.points, + estimated_time=c.estimated_time, + proof_type=c.proof_type, + proof_hint=c.proof_hint, + game=GameShort(id=c.game.id, title=c.game.title, cover_url=None), + is_generated=c.is_generated, + created_at=c.created_at, + ) + for c in challenges + ] + + @router.post("/games/{game_id}/challenges", response_model=ChallengeResponse) async def create_challenge( game_id: int, diff --git a/backend/app/api/v1/events.py b/backend/app/api/v1/events.py new file mode 100644 index 0000000..ce2a4e5 --- /dev/null +++ b/backend/app/api/v1/events.py @@ -0,0 +1,866 @@ +from datetime import datetime +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +from sqlalchemy import select, and_, or_ +from sqlalchemy.orm import selectinload + +from app.api.deps import DbSession, CurrentUser +from app.models import ( + Marathon, MarathonStatus, Participant, ParticipantRole, + Event, EventType, Activity, ActivityType, Assignment, AssignmentStatus, Challenge, + SwapRequest as SwapRequestModel, SwapRequestStatus, User, +) +from app.schemas import ( + EventCreate, EventResponse, ActiveEventResponse, EventEffects, + MessageResponse, SwapRequest, ChallengeResponse, GameShort, SwapCandidate, + SwapRequestCreate, SwapRequestResponse, SwapRequestChallengeInfo, MySwapRequests, + CommonEnemyLeaderboard, +) +from app.schemas.event import EVENT_INFO, COMMON_ENEMY_BONUSES +from app.schemas.user import UserPublic +from app.services.events import event_service + +router = APIRouter(tags=["events"]) + + +async def get_marathon_or_404(db, marathon_id: int) -> Marathon: + result = await db.execute(select(Marathon).where(Marathon.id == marathon_id)) + marathon = result.scalar_one_or_none() + if not marathon: + raise HTTPException(status_code=404, detail="Marathon not found") + return marathon + + +async def require_organizer(db, user_id: int, marathon_id: int) -> Participant: + result = await db.execute( + select(Participant).where( + Participant.user_id == user_id, + Participant.marathon_id == marathon_id, + ) + ) + participant = result.scalar_one_or_none() + if not participant: + raise HTTPException(status_code=403, detail="Not a participant") + if participant.role != ParticipantRole.ORGANIZER.value: + raise HTTPException(status_code=403, detail="Only organizers can manage events") + return participant + + +async def require_participant(db, user_id: int, marathon_id: int) -> Participant: + result = await db.execute( + select(Participant).where( + Participant.user_id == user_id, + Participant.marathon_id == marathon_id, + ) + ) + participant = result.scalar_one_or_none() + if not participant: + raise HTTPException(status_code=403, detail="Not a participant") + return participant + + +def event_to_response(event: Event) -> EventResponse: + return EventResponse( + id=event.id, + type=event.type, + start_time=event.start_time, + end_time=event.end_time, + is_active=event.is_active, + created_by=UserPublic( + id=event.created_by.id, + login=event.created_by.login, + nickname=event.created_by.nickname, + avatar_url=None, + role=event.created_by.role, + created_at=event.created_by.created_at, + ) if event.created_by else None, + data=event.data, + created_at=event.created_at, + ) + + +@router.get("/marathons/{marathon_id}/event", response_model=ActiveEventResponse) +async def get_active_event( + marathon_id: int, + current_user: CurrentUser, + db: DbSession +): + """Get currently active event for marathon""" + await get_marathon_or_404(db, marathon_id) + await require_participant(db, current_user.id, marathon_id) + + event = await event_service.get_active_event(db, marathon_id) + effects = event_service.get_event_effects(event) + time_remaining = event_service.get_time_remaining(event) + + return ActiveEventResponse( + event=event_to_response(event) if event else None, + effects=effects, + time_remaining_seconds=time_remaining, + ) + + +@router.get("/marathons/{marathon_id}/events", response_model=list[EventResponse]) +async def list_events( + marathon_id: int, + current_user: CurrentUser, + db: DbSession, + limit: int = 20, + offset: int = 0, +): + """Get event history for marathon""" + await get_marathon_or_404(db, marathon_id) + await require_participant(db, current_user.id, marathon_id) + + result = await db.execute( + select(Event) + .options(selectinload(Event.created_by)) + .where(Event.marathon_id == marathon_id) + .order_by(Event.created_at.desc()) + .limit(limit) + .offset(offset) + ) + events = result.scalars().all() + + return [event_to_response(e) for e in events] + + +@router.post("/marathons/{marathon_id}/events", response_model=EventResponse) +async def start_event( + marathon_id: int, + data: EventCreate, + current_user: CurrentUser, + db: DbSession, +): + """Start a new event (organizer only)""" + marathon = await get_marathon_or_404(db, marathon_id) + await require_organizer(db, current_user.id, marathon_id) + + if marathon.status != MarathonStatus.ACTIVE.value: + raise HTTPException(status_code=400, detail="Marathon is not active") + + # Validate common_enemy requires challenge_id + if data.type == EventType.COMMON_ENEMY.value and not data.challenge_id: + raise HTTPException( + status_code=400, + detail="Common enemy event requires challenge_id" + ) + + try: + event = await event_service.start_event( + db=db, + marathon_id=marathon_id, + event_type=data.type, + created_by_id=current_user.id, + duration_minutes=data.duration_minutes, + challenge_id=data.challenge_id, + ) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + # Log activity + event_info = EVENT_INFO.get(EventType(data.type), {}) + activity = Activity( + marathon_id=marathon_id, + user_id=current_user.id, + type=ActivityType.EVENT_START.value, + data={ + "event_type": data.type, + "event_name": event_info.get("name", data.type), + }, + ) + db.add(activity) + await db.commit() + + # Reload with relationship + await db.refresh(event, ["created_by"]) + + return event_to_response(event) + + +@router.delete("/marathons/{marathon_id}/event", response_model=MessageResponse) +async def stop_event( + marathon_id: int, + current_user: CurrentUser, + db: DbSession, +): + """Stop active event (organizer only)""" + await get_marathon_or_404(db, marathon_id) + await require_organizer(db, current_user.id, marathon_id) + + event = await event_service.get_active_event(db, marathon_id) + if not event: + raise HTTPException(status_code=404, detail="No active event") + + # Build activity data before ending event + event_info = EVENT_INFO.get(EventType(event.type), {}) + activity_data = { + "event_type": event.type, + "event_name": event_info.get("name", event.type), + "auto_closed": False, + } + + # For common_enemy, include winners in activity + if event.type == EventType.COMMON_ENEMY.value: + event_data = event.data or {} + completions = event_data.get("completions", []) + if completions: + activity_data["winners"] = [ + { + "user_id": c["user_id"], + "rank": c["rank"], + "bonus_points": COMMON_ENEMY_BONUSES.get(c["rank"], 0), + } + for c in completions[:3] # Top 3 + ] + + await event_service.end_event(db, event.id) + + # Log activity + activity = Activity( + marathon_id=marathon_id, + user_id=current_user.id, + type=ActivityType.EVENT_END.value, + data=activity_data, + ) + db.add(activity) + await db.commit() + + return MessageResponse(message="Event stopped") + + +def build_swap_request_response( + swap_req: SwapRequestModel, +) -> SwapRequestResponse: + """Build SwapRequestResponse from model with loaded relationships""" + return SwapRequestResponse( + id=swap_req.id, + status=swap_req.status, + from_user=UserPublic( + id=swap_req.from_participant.user.id, + login=swap_req.from_participant.user.login, + nickname=swap_req.from_participant.user.nickname, + avatar_url=None, + role=swap_req.from_participant.user.role, + created_at=swap_req.from_participant.user.created_at, + ), + to_user=UserPublic( + id=swap_req.to_participant.user.id, + login=swap_req.to_participant.user.login, + nickname=swap_req.to_participant.user.nickname, + avatar_url=None, + role=swap_req.to_participant.user.role, + created_at=swap_req.to_participant.user.created_at, + ), + from_challenge=SwapRequestChallengeInfo( + title=swap_req.from_assignment.challenge.title, + description=swap_req.from_assignment.challenge.description, + points=swap_req.from_assignment.challenge.points, + difficulty=swap_req.from_assignment.challenge.difficulty, + game_title=swap_req.from_assignment.challenge.game.title, + ), + to_challenge=SwapRequestChallengeInfo( + title=swap_req.to_assignment.challenge.title, + description=swap_req.to_assignment.challenge.description, + points=swap_req.to_assignment.challenge.points, + difficulty=swap_req.to_assignment.challenge.difficulty, + game_title=swap_req.to_assignment.challenge.game.title, + ), + created_at=swap_req.created_at, + responded_at=swap_req.responded_at, + ) + + +@router.post("/marathons/{marathon_id}/swap-requests", response_model=SwapRequestResponse) +async def create_swap_request( + marathon_id: int, + data: SwapRequestCreate, + current_user: CurrentUser, + db: DbSession, +): + """Create a swap request to another participant (requires their confirmation)""" + await get_marathon_or_404(db, marathon_id) + participant = await require_participant(db, current_user.id, marathon_id) + + # Check active swap event + event = await event_service.get_active_event(db, marathon_id) + if not event or event.type != EventType.SWAP.value: + raise HTTPException(status_code=400, detail="No active swap event") + + # Get target participant + result = await db.execute( + select(Participant) + .options(selectinload(Participant.user)) + .where( + Participant.id == data.target_participant_id, + Participant.marathon_id == marathon_id, + ) + ) + target = result.scalar_one_or_none() + if not target: + raise HTTPException(status_code=404, detail="Target participant not found") + + if target.id == participant.id: + raise HTTPException(status_code=400, detail="Cannot swap with yourself") + + # Get both active assignments + result = await db.execute( + select(Assignment) + .options( + selectinload(Assignment.challenge).selectinload(Challenge.game) + ) + .where( + Assignment.participant_id == participant.id, + Assignment.status == AssignmentStatus.ACTIVE.value, + ) + ) + my_assignment = result.scalar_one_or_none() + + result = await db.execute( + select(Assignment) + .options( + selectinload(Assignment.challenge).selectinload(Challenge.game) + ) + .where( + Assignment.participant_id == target.id, + Assignment.status == AssignmentStatus.ACTIVE.value, + ) + ) + target_assignment = result.scalar_one_or_none() + + if not my_assignment or not target_assignment: + raise HTTPException( + status_code=400, + detail="Both participants must have active assignments to swap" + ) + + # Check if there's already a pending request between these participants + result = await db.execute( + select(SwapRequestModel).where( + SwapRequestModel.event_id == event.id, + SwapRequestModel.status == SwapRequestStatus.PENDING.value, + or_( + and_( + SwapRequestModel.from_participant_id == participant.id, + SwapRequestModel.to_participant_id == target.id, + ), + and_( + SwapRequestModel.from_participant_id == target.id, + SwapRequestModel.to_participant_id == participant.id, + ), + ) + ) + ) + existing = result.scalar_one_or_none() + if existing: + raise HTTPException( + status_code=400, + detail="A pending swap request already exists between you and this participant" + ) + + # Create swap request + swap_request = SwapRequestModel( + event_id=event.id, + from_participant_id=participant.id, + to_participant_id=target.id, + from_assignment_id=my_assignment.id, + to_assignment_id=target_assignment.id, + status=SwapRequestStatus.PENDING.value, + ) + db.add(swap_request) + await db.commit() + await db.refresh(swap_request) + + # Load relationships for response + result = await db.execute( + select(SwapRequestModel) + .options( + selectinload(SwapRequestModel.from_participant).selectinload(Participant.user), + selectinload(SwapRequestModel.to_participant).selectinload(Participant.user), + selectinload(SwapRequestModel.from_assignment) + .selectinload(Assignment.challenge) + .selectinload(Challenge.game), + selectinload(SwapRequestModel.to_assignment) + .selectinload(Assignment.challenge) + .selectinload(Challenge.game), + ) + .where(SwapRequestModel.id == swap_request.id) + ) + swap_request = result.scalar_one() + + return build_swap_request_response(swap_request) + + +@router.get("/marathons/{marathon_id}/swap-requests", response_model=MySwapRequests) +async def get_my_swap_requests( + marathon_id: int, + current_user: CurrentUser, + db: DbSession, +): + """Get current user's incoming and outgoing swap requests""" + await get_marathon_or_404(db, marathon_id) + participant = await require_participant(db, current_user.id, marathon_id) + + # Check active swap event + event = await event_service.get_active_event(db, marathon_id) + if not event or event.type != EventType.SWAP.value: + return MySwapRequests(incoming=[], outgoing=[]) + + # Get all pending requests for this event involving this participant + result = await db.execute( + select(SwapRequestModel) + .options( + selectinload(SwapRequestModel.from_participant).selectinload(Participant.user), + selectinload(SwapRequestModel.to_participant).selectinload(Participant.user), + selectinload(SwapRequestModel.from_assignment) + .selectinload(Assignment.challenge) + .selectinload(Challenge.game), + selectinload(SwapRequestModel.to_assignment) + .selectinload(Assignment.challenge) + .selectinload(Challenge.game), + ) + .where( + SwapRequestModel.event_id == event.id, + SwapRequestModel.status == SwapRequestStatus.PENDING.value, + or_( + SwapRequestModel.from_participant_id == participant.id, + SwapRequestModel.to_participant_id == participant.id, + ) + ) + .order_by(SwapRequestModel.created_at.desc()) + ) + requests = result.scalars().all() + + incoming = [] + outgoing = [] + for req in requests: + response = build_swap_request_response(req) + if req.to_participant_id == participant.id: + incoming.append(response) + else: + outgoing.append(response) + + return MySwapRequests(incoming=incoming, outgoing=outgoing) + + +@router.post("/marathons/{marathon_id}/swap-requests/{request_id}/accept", response_model=MessageResponse) +async def accept_swap_request( + marathon_id: int, + request_id: int, + current_user: CurrentUser, + db: DbSession, +): + """Accept a swap request (performs the actual swap)""" + await get_marathon_or_404(db, marathon_id) + participant = await require_participant(db, current_user.id, marathon_id) + + # Check active swap event + event = await event_service.get_active_event(db, marathon_id) + if not event or event.type != EventType.SWAP.value: + raise HTTPException(status_code=400, detail="No active swap event") + + # Get the swap request + result = await db.execute( + select(SwapRequestModel) + .options( + selectinload(SwapRequestModel.from_participant).selectinload(Participant.user), + selectinload(SwapRequestModel.to_participant), + selectinload(SwapRequestModel.from_assignment), + selectinload(SwapRequestModel.to_assignment), + ) + .where( + SwapRequestModel.id == request_id, + SwapRequestModel.event_id == event.id, + ) + ) + swap_request = result.scalar_one_or_none() + if not swap_request: + raise HTTPException(status_code=404, detail="Swap request not found") + + # Check that current user is the target + if swap_request.to_participant_id != participant.id: + raise HTTPException(status_code=403, detail="You can only accept requests sent to you") + + if swap_request.status != SwapRequestStatus.PENDING.value: + raise HTTPException(status_code=400, detail="This request is no longer pending") + + # Verify both assignments are still active + result = await db.execute( + select(Assignment).where(Assignment.id == swap_request.from_assignment_id) + ) + from_assignment = result.scalar_one_or_none() + + result = await db.execute( + select(Assignment).where(Assignment.id == swap_request.to_assignment_id) + ) + to_assignment = result.scalar_one_or_none() + + if not from_assignment or not to_assignment: + swap_request.status = SwapRequestStatus.CANCELLED.value + swap_request.responded_at = datetime.utcnow() + await db.commit() + raise HTTPException(status_code=400, detail="One or both assignments no longer exist") + + if from_assignment.status != AssignmentStatus.ACTIVE.value or to_assignment.status != AssignmentStatus.ACTIVE.value: + swap_request.status = SwapRequestStatus.CANCELLED.value + swap_request.responded_at = datetime.utcnow() + await db.commit() + raise HTTPException(status_code=400, detail="One or both assignments are no longer active") + + # Perform the swap + from_challenge_id = from_assignment.challenge_id + from_assignment.challenge_id = to_assignment.challenge_id + to_assignment.challenge_id = from_challenge_id + + # Update request status + swap_request.status = SwapRequestStatus.ACCEPTED.value + swap_request.responded_at = datetime.utcnow() + + # Cancel any other pending requests involving these participants in this event + await db.execute( + select(SwapRequestModel) + .where( + SwapRequestModel.event_id == event.id, + SwapRequestModel.status == SwapRequestStatus.PENDING.value, + SwapRequestModel.id != request_id, + or_( + SwapRequestModel.from_participant_id.in_([swap_request.from_participant_id, swap_request.to_participant_id]), + SwapRequestModel.to_participant_id.in_([swap_request.from_participant_id, swap_request.to_participant_id]), + ) + ) + ) + # Update those to cancelled + from sqlalchemy import update + await db.execute( + update(SwapRequestModel) + .where( + SwapRequestModel.event_id == event.id, + SwapRequestModel.status == SwapRequestStatus.PENDING.value, + SwapRequestModel.id != request_id, + or_( + SwapRequestModel.from_participant_id.in_([swap_request.from_participant_id, swap_request.to_participant_id]), + SwapRequestModel.to_participant_id.in_([swap_request.from_participant_id, swap_request.to_participant_id]), + ) + ) + .values(status=SwapRequestStatus.CANCELLED.value, responded_at=datetime.utcnow()) + ) + + # Log activity + activity = Activity( + marathon_id=marathon_id, + user_id=current_user.id, + type=ActivityType.SWAP.value, + data={ + "swapped_with_user_id": swap_request.from_participant.user_id, + "swapped_with_nickname": swap_request.from_participant.user.nickname, + }, + ) + db.add(activity) + + await db.commit() + + return MessageResponse(message="Swap completed successfully!") + + +@router.post("/marathons/{marathon_id}/swap-requests/{request_id}/decline", response_model=MessageResponse) +async def decline_swap_request( + marathon_id: int, + request_id: int, + current_user: CurrentUser, + db: DbSession, +): + """Decline a swap request""" + await get_marathon_or_404(db, marathon_id) + participant = await require_participant(db, current_user.id, marathon_id) + + # Check active swap event (allow declining even if event ended) + event = await event_service.get_active_event(db, marathon_id) + + # Get the swap request + result = await db.execute( + select(SwapRequestModel).where(SwapRequestModel.id == request_id) + ) + swap_request = result.scalar_one_or_none() + if not swap_request: + raise HTTPException(status_code=404, detail="Swap request not found") + + # Check that current user is the target + if swap_request.to_participant_id != participant.id: + raise HTTPException(status_code=403, detail="You can only decline requests sent to you") + + if swap_request.status != SwapRequestStatus.PENDING.value: + raise HTTPException(status_code=400, detail="This request is no longer pending") + + # Update status + swap_request.status = SwapRequestStatus.DECLINED.value + swap_request.responded_at = datetime.utcnow() + + await db.commit() + + return MessageResponse(message="Swap request declined") + + +@router.delete("/marathons/{marathon_id}/swap-requests/{request_id}", response_model=MessageResponse) +async def cancel_swap_request( + marathon_id: int, + request_id: int, + current_user: CurrentUser, + db: DbSession, +): + """Cancel your own outgoing swap request""" + await get_marathon_or_404(db, marathon_id) + participant = await require_participant(db, current_user.id, marathon_id) + + # Get the swap request + result = await db.execute( + select(SwapRequestModel).where(SwapRequestModel.id == request_id) + ) + swap_request = result.scalar_one_or_none() + if not swap_request: + raise HTTPException(status_code=404, detail="Swap request not found") + + # Check that current user is the sender + if swap_request.from_participant_id != participant.id: + raise HTTPException(status_code=403, detail="You can only cancel your own requests") + + if swap_request.status != SwapRequestStatus.PENDING.value: + raise HTTPException(status_code=400, detail="This request is no longer pending") + + # Update status + swap_request.status = SwapRequestStatus.CANCELLED.value + swap_request.responded_at = datetime.utcnow() + + await db.commit() + + return MessageResponse(message="Swap request cancelled") + + +@router.post("/marathons/{marathon_id}/rematch/{assignment_id}", response_model=MessageResponse) +async def rematch_assignment( + marathon_id: int, + assignment_id: int, + current_user: CurrentUser, + db: DbSession, +): + """Retry a dropped assignment (during rematch event)""" + await get_marathon_or_404(db, marathon_id) + participant = await require_participant(db, current_user.id, marathon_id) + + # Check active rematch event + event = await event_service.get_active_event(db, marathon_id) + if not event or event.type != EventType.REMATCH.value: + raise HTTPException(status_code=400, detail="No active rematch event") + + # Check no current active assignment + result = await db.execute( + select(Assignment).where( + Assignment.participant_id == participant.id, + Assignment.status == AssignmentStatus.ACTIVE.value, + ) + ) + if result.scalar_one_or_none(): + raise HTTPException(status_code=400, detail="You already have an active assignment") + + # Get the dropped assignment + result = await db.execute( + select(Assignment) + .options(selectinload(Assignment.challenge)) + .where( + Assignment.id == assignment_id, + Assignment.participant_id == participant.id, + Assignment.status == AssignmentStatus.DROPPED.value, + ) + ) + dropped = result.scalar_one_or_none() + if not dropped: + raise HTTPException(status_code=404, detail="Dropped assignment not found") + + # Create new assignment for the same challenge (with rematch event_type for 50% points) + new_assignment = Assignment( + participant_id=participant.id, + challenge_id=dropped.challenge_id, + status=AssignmentStatus.ACTIVE.value, + event_type=EventType.REMATCH.value, + ) + db.add(new_assignment) + + # Log activity + activity = Activity( + marathon_id=marathon_id, + user_id=current_user.id, + type=ActivityType.REMATCH.value, + data={ + "challenge": dropped.challenge.title, + "original_assignment_id": assignment_id, + }, + ) + db.add(activity) + + await db.commit() + + return MessageResponse(message="Rematch started! Complete for 50% points") + + +class DroppedAssignmentResponse(BaseModel): + id: int + challenge: ChallengeResponse + dropped_at: datetime + + class Config: + from_attributes = True + + +@router.get("/marathons/{marathon_id}/dropped-assignments", response_model=list[DroppedAssignmentResponse]) +async def get_dropped_assignments( + marathon_id: int, + current_user: CurrentUser, + db: DbSession, +): + """Get dropped assignments that can be rematched""" + await get_marathon_or_404(db, marathon_id) + participant = await require_participant(db, current_user.id, marathon_id) + + result = await db.execute( + select(Assignment) + .options( + selectinload(Assignment.challenge).selectinload(Challenge.game) + ) + .where( + Assignment.participant_id == participant.id, + Assignment.status == AssignmentStatus.DROPPED.value, + ) + .order_by(Assignment.started_at.desc()) + ) + dropped = result.scalars().all() + + return [ + DroppedAssignmentResponse( + id=a.id, + challenge=ChallengeResponse( + id=a.challenge.id, + title=a.challenge.title, + description=a.challenge.description, + type=a.challenge.type, + difficulty=a.challenge.difficulty, + points=a.challenge.points, + estimated_time=a.challenge.estimated_time, + proof_type=a.challenge.proof_type, + proof_hint=a.challenge.proof_hint, + game=GameShort( + id=a.challenge.game.id, + title=a.challenge.game.title, + cover_url=None, + ), + is_generated=a.challenge.is_generated, + created_at=a.challenge.created_at, + ), + dropped_at=a.completed_at or a.started_at, + ) + for a in dropped + ] + + +@router.get("/marathons/{marathon_id}/swap-candidates", response_model=list[SwapCandidate]) +async def get_swap_candidates( + marathon_id: int, + current_user: CurrentUser, + db: DbSession, +): + """Get participants with active assignments available for swap (during swap event)""" + await get_marathon_or_404(db, marathon_id) + participant = await require_participant(db, current_user.id, marathon_id) + + # Check active swap event + event = await event_service.get_active_event(db, marathon_id) + if not event or event.type != EventType.SWAP.value: + raise HTTPException(status_code=400, detail="No active swap event") + + # Get all participants except current user with active assignments + from app.models import Game + result = await db.execute( + select(Participant, Assignment, Challenge, Game) + .join(Assignment, Assignment.participant_id == Participant.id) + .join(Challenge, Assignment.challenge_id == Challenge.id) + .join(Game, Challenge.game_id == Game.id) + .options(selectinload(Participant.user)) + .where( + Participant.marathon_id == marathon_id, + Participant.id != participant.id, + Assignment.status == AssignmentStatus.ACTIVE.value, + ) + ) + rows = result.all() + + return [ + SwapCandidate( + participant_id=p.id, + user=UserPublic( + id=p.user.id, + login=p.user.login, + nickname=p.user.nickname, + avatar_url=None, + role=p.user.role, + created_at=p.user.created_at, + ), + challenge_title=challenge.title, + challenge_description=challenge.description, + challenge_points=challenge.points, + challenge_difficulty=challenge.difficulty, + game_title=game.title, + ) + for p, assignment, challenge, game in rows + ] + + +@router.get("/marathons/{marathon_id}/common-enemy-leaderboard", response_model=list[CommonEnemyLeaderboard]) +async def get_common_enemy_leaderboard( + marathon_id: int, + current_user: CurrentUser, + db: DbSession, +): + """Get leaderboard for common enemy event (who completed the challenge)""" + await get_marathon_or_404(db, marathon_id) + await require_participant(db, current_user.id, marathon_id) + + # Get active common enemy event + event = await event_service.get_active_event(db, marathon_id) + if not event or event.type != EventType.COMMON_ENEMY.value: + return [] + + # Get completions from event data + data = event.data or {} + completions = data.get("completions", []) + + if not completions: + return [] + + # Get user info for all participants who completed + user_ids = [c["user_id"] for c in completions] + result = await db.execute( + select(User).where(User.id.in_(user_ids)) + ) + users_by_id = {u.id: u for u in result.scalars().all()} + + # Build leaderboard + leaderboard = [] + for completion in completions: + user = users_by_id.get(completion["user_id"]) + if user: + leaderboard.append( + CommonEnemyLeaderboard( + participant_id=completion["participant_id"], + user=UserPublic( + id=user.id, + login=user.login, + nickname=user.nickname, + avatar_url=None, + role=user.role, + created_at=user.created_at, + ), + completed_at=completion.get("completed_at"), + rank=completion.get("rank"), + bonus_points=COMMON_ENEMY_BONUSES.get(completion.get("rank", 0), 0), + ) + ) + + return leaderboard diff --git a/backend/app/api/v1/marathons.py b/backend/app/api/v1/marathons.py index c050acb..d113ce8 100644 --- a/backend/app/api/v1/marathons.py +++ b/backend/app/api/v1/marathons.py @@ -170,6 +170,7 @@ async def create_marathon( invite_code=marathon.invite_code, is_public=marathon.is_public, game_proposal_mode=marathon.game_proposal_mode, + auto_events_enabled=marathon.auto_events_enabled, start_date=marathon.start_date, end_date=marathon.end_date, participants_count=1, @@ -206,6 +207,7 @@ async def get_marathon(marathon_id: int, current_user: CurrentUser, db: DbSessio invite_code=marathon.invite_code, is_public=marathon.is_public, game_proposal_mode=marathon.game_proposal_mode, + auto_events_enabled=marathon.auto_events_enabled, start_date=marathon.start_date, end_date=marathon.end_date, participants_count=participants_count, @@ -240,6 +242,8 @@ async def update_marathon( marathon.is_public = data.is_public if data.game_proposal_mode is not None: marathon.game_proposal_mode = data.game_proposal_mode + if data.auto_events_enabled is not None: + marathon.auto_events_enabled = data.auto_events_enabled await db.commit() diff --git a/backend/app/api/v1/wheel.py b/backend/app/api/v1/wheel.py index f1bcd14..145686c 100644 --- a/backend/app/api/v1/wheel.py +++ b/backend/app/api/v1/wheel.py @@ -10,13 +10,15 @@ from app.api.deps import DbSession, CurrentUser from app.core.config import settings from app.models import ( Marathon, MarathonStatus, Game, Challenge, Participant, - Assignment, AssignmentStatus, Activity, ActivityType + Assignment, AssignmentStatus, Activity, ActivityType, + EventType, Difficulty ) from app.schemas import ( SpinResult, AssignmentResponse, CompleteResult, DropResult, GameResponse, ChallengeResponse, GameShort, UserPublic, MessageResponse ) from app.services.points import PointsService +from app.services.events import event_service router = APIRouter(tags=["wheel"]) @@ -69,46 +71,91 @@ async def spin_wheel(marathon_id: int, current_user: CurrentUser, db: DbSession) if active: raise HTTPException(status_code=400, detail="You already have an active assignment") - # Get all games with challenges - result = await db.execute( - select(Game) - .options(selectinload(Game.challenges)) - .where(Game.marathon_id == marathon_id) - ) - games = [g for g in result.scalars().all() if g.challenges] + # Check active event + active_event = await event_service.get_active_event(db, marathon_id) - if not games: - raise HTTPException(status_code=400, detail="No games with challenges available") + game = None + challenge = None - # Random selection - game = random.choice(games) - challenge = random.choice(game.challenges) + # Handle special event cases + if active_event: + if active_event.type == EventType.JACKPOT.value: + # Jackpot: Get hard challenge only + challenge = await event_service.get_random_hard_challenge(db, marathon_id) + if challenge: + # Load game for challenge + result = await db.execute( + select(Game).where(Game.id == challenge.game_id) + ) + game = result.scalar_one_or_none() + # Consume jackpot (one-time use) + await event_service.consume_jackpot(db, active_event.id) - # Create assignment + elif active_event.type == EventType.COMMON_ENEMY.value: + # Common enemy: Everyone gets same challenge (if not already completed) + event_data = active_event.data or {} + completions = event_data.get("completions", []) + already_completed = any(c["participant_id"] == participant.id for c in completions) + + if not already_completed: + challenge = await event_service.get_common_enemy_challenge(db, active_event) + if challenge: + game = challenge.game + + # Normal random selection if no special event handling + if not game or not challenge: + result = await db.execute( + select(Game) + .options(selectinload(Game.challenges)) + .where(Game.marathon_id == marathon_id) + ) + games = [g for g in result.scalars().all() if g.challenges] + + if not games: + raise HTTPException(status_code=400, detail="No games with challenges available") + + game = random.choice(games) + challenge = random.choice(game.challenges) + + # Create assignment (store event_type for jackpot multiplier on completion) assignment = Assignment( participant_id=participant.id, challenge_id=challenge.id, status=AssignmentStatus.ACTIVE.value, + event_type=active_event.type if active_event else None, ) db.add(assignment) # Log activity + activity_data = { + "game": game.title, + "challenge": challenge.title, + } + if active_event: + activity_data["event_type"] = active_event.type + activity = Activity( marathon_id=marathon_id, user_id=current_user.id, type=ActivityType.SPIN.value, - data={ - "game": game.title, - "challenge": challenge.title, - }, + data=activity_data, ) db.add(activity) await db.commit() await db.refresh(assignment) - # Calculate drop penalty - drop_penalty = points_service.calculate_drop_penalty(participant.drop_count) + # Calculate drop penalty (considers active event for double_risk) + drop_penalty = points_service.calculate_drop_penalty(participant.drop_count, active_event) + + # Get challenges count (avoid lazy loading in async context) + challenges_count = 0 + if 'challenges' in game.__dict__: + challenges_count = len(game.challenges) + else: + challenges_count = await db.scalar( + select(func.count()).select_from(Challenge).where(Challenge.game_id == game.id) + ) return SpinResult( assignment_id=assignment.id, @@ -119,7 +166,7 @@ async def spin_wheel(marathon_id: int, current_user: CurrentUser, db: DbSession) download_url=game.download_url, genre=game.genre, added_by=None, - challenges_count=len(game.challenges), + challenges_count=challenges_count, created_at=game.created_at, ), challenge=ChallengeResponse( @@ -246,9 +293,41 @@ async def complete_assignment( participant = assignment.participant challenge = assignment.challenge - total_points, streak_bonus = points_service.calculate_completion_points( - challenge.points, participant.current_streak + # Get marathon_id for activity and event check + result = await db.execute( + select(Challenge).options(selectinload(Challenge.game)).where(Challenge.id == challenge.id) ) + full_challenge = result.scalar_one() + marathon_id = full_challenge.game.marathon_id + + # Check active event for point multipliers + active_event = await event_service.get_active_event(db, marathon_id) + + # For jackpot/rematch: use the event_type stored in assignment (since event may be over) + # For other events: use the currently active event + effective_event = active_event + + # Handle assignment-level event types (jackpot, rematch) + if assignment.event_type in [EventType.JACKPOT.value, EventType.REMATCH.value]: + # Create a mock event object for point calculation + class MockEvent: + def __init__(self, event_type): + self.type = event_type + effective_event = MockEvent(assignment.event_type) + + total_points, streak_bonus, event_bonus = points_service.calculate_completion_points( + challenge.points, participant.current_streak, effective_event + ) + + # Handle common enemy bonus + common_enemy_bonus = 0 + common_enemy_closed = False + common_enemy_winners = None + if active_event and active_event.type == EventType.COMMON_ENEMY.value: + common_enemy_bonus, common_enemy_closed, common_enemy_winners = await event_service.record_common_enemy_completion( + db, active_event, participant.id, current_user.id + ) + total_points += common_enemy_bonus # Update assignment assignment.status = AssignmentStatus.COMPLETED.value @@ -261,25 +340,53 @@ async def complete_assignment( participant.current_streak += 1 participant.drop_count = 0 # Reset drop counter on success - # Get marathon_id for activity - result = await db.execute( - select(Challenge).options(selectinload(Challenge.game)).where(Challenge.id == challenge.id) - ) - full_challenge = result.scalar_one() - # Log activity + activity_data = { + "challenge": challenge.title, + "points": total_points, + "streak": participant.current_streak, + } + # Log event info (use assignment's event_type for jackpot/rematch, active_event for others) + if assignment.event_type in [EventType.JACKPOT.value, EventType.REMATCH.value]: + activity_data["event_type"] = assignment.event_type + activity_data["event_bonus"] = event_bonus + elif active_event: + activity_data["event_type"] = active_event.type + activity_data["event_bonus"] = event_bonus + if common_enemy_bonus: + activity_data["common_enemy_bonus"] = common_enemy_bonus + activity = Activity( - marathon_id=full_challenge.game.marathon_id, + marathon_id=marathon_id, user_id=current_user.id, type=ActivityType.COMPLETE.value, - data={ - "challenge": challenge.title, - "points": total_points, - "streak": participant.current_streak, - }, + data=activity_data, ) db.add(activity) + # If common enemy event auto-closed, log the event end with winners + if common_enemy_closed and common_enemy_winners: + from app.schemas.event import EVENT_INFO, COMMON_ENEMY_BONUSES + event_end_activity = Activity( + marathon_id=marathon_id, + user_id=current_user.id, # Last completer triggers the close + type=ActivityType.EVENT_END.value, + data={ + "event_type": EventType.COMMON_ENEMY.value, + "event_name": EVENT_INFO.get(EventType.COMMON_ENEMY, {}).get("name", "Общий враг"), + "auto_closed": True, + "winners": [ + { + "user_id": w["user_id"], + "rank": w["rank"], + "bonus_points": COMMON_ENEMY_BONUSES.get(w["rank"], 0), + } + for w in common_enemy_winners + ], + }, + ) + db.add(event_end_activity) + await db.commit() return CompleteResult( @@ -314,9 +421,13 @@ async def drop_assignment(assignment_id: int, current_user: CurrentUser, db: DbS raise HTTPException(status_code=400, detail="Assignment is not active") participant = assignment.participant + marathon_id = assignment.challenge.game.marathon_id - # Calculate penalty - penalty = points_service.calculate_drop_penalty(participant.drop_count) + # Check active event for free drops (double_risk) + active_event = await event_service.get_active_event(db, marathon_id) + + # Calculate penalty (0 if double_risk event is active) + penalty = points_service.calculate_drop_penalty(participant.drop_count, active_event) # Update assignment assignment.status = AssignmentStatus.DROPPED.value @@ -328,14 +439,20 @@ async def drop_assignment(assignment_id: int, current_user: CurrentUser, db: DbS participant.drop_count += 1 # Log activity + activity_data = { + "challenge": assignment.challenge.title, + "penalty": penalty, + } + if active_event: + activity_data["event_type"] = active_event.type + if active_event.type == EventType.DOUBLE_RISK.value: + activity_data["free_drop"] = True + activity = Activity( - marathon_id=assignment.challenge.game.marathon_id, + marathon_id=marathon_id, user_id=current_user.id, type=ActivityType.DROP.value, - data={ - "challenge": assignment.challenge.title, - "penalty": penalty, - }, + data=activity_data, ) db.add(activity) diff --git a/backend/app/main.py b/backend/app/main.py index 4b4aa22..c858888 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -6,8 +6,9 @@ from fastapi.staticfiles import StaticFiles from pathlib import Path from app.core.config import settings -from app.core.database import engine, Base +from app.core.database import engine, Base, async_session_maker from app.api.v1 import router as api_router +from app.services.event_scheduler import event_scheduler @asynccontextmanager @@ -22,9 +23,13 @@ async def lifespan(app: FastAPI): (upload_dir / "covers").mkdir(parents=True, exist_ok=True) (upload_dir / "proofs").mkdir(parents=True, exist_ok=True) + # Start event scheduler + await event_scheduler.start(async_session_maker) + yield # Shutdown + await event_scheduler.stop() await engine.dispose() diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 192c3b8..e88ea9a 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -5,6 +5,8 @@ from app.models.game import Game, GameStatus from app.models.challenge import Challenge, ChallengeType, Difficulty, ProofType from app.models.assignment import Assignment, AssignmentStatus from app.models.activity import Activity, ActivityType +from app.models.event import Event, EventType +from app.models.swap_request import SwapRequest, SwapRequestStatus __all__ = [ "User", @@ -24,4 +26,8 @@ __all__ = [ "AssignmentStatus", "Activity", "ActivityType", + "Event", + "EventType", + "SwapRequest", + "SwapRequestStatus", ] diff --git a/backend/app/models/activity.py b/backend/app/models/activity.py index e20d844..be2c067 100644 --- a/backend/app/models/activity.py +++ b/backend/app/models/activity.py @@ -16,6 +16,10 @@ class ActivityType(str, Enum): ADD_GAME = "add_game" APPROVE_GAME = "approve_game" REJECT_GAME = "reject_game" + EVENT_START = "event_start" + EVENT_END = "event_end" + SWAP = "swap" + REMATCH = "rematch" class Activity(Base): diff --git a/backend/app/models/assignment.py b/backend/app/models/assignment.py index 29be9c3..212ccd1 100644 --- a/backend/app/models/assignment.py +++ b/backend/app/models/assignment.py @@ -19,6 +19,7 @@ class Assignment(Base): participant_id: Mapped[int] = mapped_column(ForeignKey("participants.id", ondelete="CASCADE"), index=True) challenge_id: Mapped[int] = mapped_column(ForeignKey("challenges.id", ondelete="CASCADE")) status: Mapped[str] = mapped_column(String(20), default=AssignmentStatus.ACTIVE.value) + event_type: Mapped[str | None] = mapped_column(String(30), nullable=True) # Event type when assignment was created proof_path: Mapped[str | None] = mapped_column(String(500), nullable=True) proof_url: Mapped[str | None] = mapped_column(Text, nullable=True) proof_comment: Mapped[str | None] = mapped_column(Text, nullable=True) diff --git a/backend/app/models/event.py b/backend/app/models/event.py new file mode 100644 index 0000000..353e436 --- /dev/null +++ b/backend/app/models/event.py @@ -0,0 +1,39 @@ +from datetime import datetime +from enum import Enum +from sqlalchemy import String, DateTime, ForeignKey, JSON, Boolean +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.core.database import Base + + +class EventType(str, Enum): + GOLDEN_HOUR = "golden_hour" # x1.5 очков + COMMON_ENEMY = "common_enemy" # общий челлендж для всех + DOUBLE_RISK = "double_risk" # дропы бесплатны, x0.5 очков + JACKPOT = "jackpot" # x3 за сложный челлендж + SWAP = "swap" # обмен заданиями + REMATCH = "rematch" # реванш проваленного + + +class Event(Base): + __tablename__ = "events" + + id: Mapped[int] = mapped_column(primary_key=True) + marathon_id: Mapped[int] = mapped_column( + ForeignKey("marathons.id", ondelete="CASCADE"), + index=True + ) + type: Mapped[str] = mapped_column(String(30), nullable=False) + start_time: Mapped[datetime] = mapped_column(DateTime, nullable=False) + end_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) + is_active: Mapped[bool] = mapped_column(Boolean, default=True) + created_by_id: Mapped[int | None] = mapped_column( + ForeignKey("users.id", ondelete="SET NULL"), + nullable=True + ) + data: Mapped[dict | None] = mapped_column(JSON, nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) + + # Relationships + marathon: Mapped["Marathon"] = relationship("Marathon", back_populates="events") + created_by: Mapped["User | None"] = relationship("User") diff --git a/backend/app/models/marathon.py b/backend/app/models/marathon.py index 3f277bf..8174798 100644 --- a/backend/app/models/marathon.py +++ b/backend/app/models/marathon.py @@ -30,6 +30,7 @@ class Marathon(Base): game_proposal_mode: Mapped[str] = mapped_column(String(20), default=GameProposalMode.ALL_PARTICIPANTS.value) start_date: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) end_date: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) + auto_events_enabled: Mapped[bool] = mapped_column(Boolean, default=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) # Relationships @@ -53,3 +54,8 @@ class Marathon(Base): back_populates="marathon", cascade="all, delete-orphan" ) + events: Mapped[list["Event"]] = relationship( + "Event", + back_populates="marathon", + cascade="all, delete-orphan" + ) diff --git a/backend/app/models/swap_request.py b/backend/app/models/swap_request.py new file mode 100644 index 0000000..fecc85a --- /dev/null +++ b/backend/app/models/swap_request.py @@ -0,0 +1,62 @@ +from datetime import datetime +from enum import Enum +from sqlalchemy import DateTime, ForeignKey, String +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.core.database import Base + + +class SwapRequestStatus(str, Enum): + PENDING = "pending" + ACCEPTED = "accepted" + DECLINED = "declined" + CANCELLED = "cancelled" # Cancelled by requester or event ended + + +class SwapRequest(Base): + __tablename__ = "swap_requests" + + id: Mapped[int] = mapped_column(primary_key=True) + event_id: Mapped[int] = mapped_column( + ForeignKey("events.id", ondelete="CASCADE"), + index=True + ) + from_participant_id: Mapped[int] = mapped_column( + ForeignKey("participants.id", ondelete="CASCADE"), + index=True + ) + to_participant_id: Mapped[int] = mapped_column( + ForeignKey("participants.id", ondelete="CASCADE"), + index=True + ) + from_assignment_id: Mapped[int] = mapped_column( + ForeignKey("assignments.id", ondelete="CASCADE") + ) + to_assignment_id: Mapped[int] = mapped_column( + ForeignKey("assignments.id", ondelete="CASCADE") + ) + status: Mapped[str] = mapped_column( + String(20), + default=SwapRequestStatus.PENDING.value + ) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) + responded_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) + + # Relationships + event: Mapped["Event"] = relationship("Event") + from_participant: Mapped["Participant"] = relationship( + "Participant", + foreign_keys=[from_participant_id] + ) + to_participant: Mapped["Participant"] = relationship( + "Participant", + foreign_keys=[to_participant_id] + ) + from_assignment: Mapped["Assignment"] = relationship( + "Assignment", + foreign_keys=[from_assignment_id] + ) + to_assignment: Mapped["Assignment"] = relationship( + "Assignment", + foreign_keys=[to_assignment_id] + ) diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py index 9b29139..b36bec9 100644 --- a/backend/app/schemas/__init__.py +++ b/backend/app/schemas/__init__.py @@ -46,6 +46,21 @@ from app.schemas.activity import ( ActivityResponse, FeedResponse, ) +from app.schemas.event import ( + EventCreate, + EventResponse, + EventEffects, + ActiveEventResponse, + SwapRequest, + SwapCandidate, + CommonEnemyLeaderboard, + EVENT_INFO, + COMMON_ENEMY_BONUSES, + SwapRequestCreate, + SwapRequestResponse, + SwapRequestChallengeInfo, + MySwapRequests, +) from app.schemas.common import ( MessageResponse, ErrorResponse, @@ -95,6 +110,20 @@ __all__ = [ # Activity "ActivityResponse", "FeedResponse", + # Event + "EventCreate", + "EventResponse", + "EventEffects", + "ActiveEventResponse", + "SwapRequest", + "SwapCandidate", + "CommonEnemyLeaderboard", + "EVENT_INFO", + "COMMON_ENEMY_BONUSES", + "SwapRequestCreate", + "SwapRequestResponse", + "SwapRequestChallengeInfo", + "MySwapRequests", # Common "MessageResponse", "ErrorResponse", diff --git a/backend/app/schemas/event.py b/backend/app/schemas/event.py new file mode 100644 index 0000000..ee96f48 --- /dev/null +++ b/backend/app/schemas/event.py @@ -0,0 +1,174 @@ +from datetime import datetime +from pydantic import BaseModel, Field +from typing import Literal + +from app.models.event import EventType +from app.schemas.user import UserPublic + + +# Event type literals for Pydantic +EventTypeLiteral = Literal[ + "golden_hour", + "common_enemy", + "double_risk", + "jackpot", + "swap", + "rematch", +] + + +class EventCreate(BaseModel): + type: EventTypeLiteral + duration_minutes: int | None = Field( + None, + description="Duration in minutes. If not provided, uses default for event type." + ) + challenge_id: int | None = Field( + None, + description="For common_enemy event - the challenge everyone will get" + ) + + +class EventEffects(BaseModel): + points_multiplier: float = 1.0 + drop_free: bool = False + special_action: str | None = None # "swap", "rematch" + description: str = "" + + +class EventResponse(BaseModel): + id: int + type: EventTypeLiteral + start_time: datetime + end_time: datetime | None + is_active: bool + created_by: UserPublic | None + data: dict | None + created_at: datetime + + class Config: + from_attributes = True + + +class ActiveEventResponse(BaseModel): + event: EventResponse | None + effects: EventEffects + time_remaining_seconds: int | None = None + + +class SwapRequest(BaseModel): + target_participant_id: int + + +class CommonEnemyLeaderboard(BaseModel): + participant_id: int + user: UserPublic + completed_at: datetime | None + rank: int | None + bonus_points: int + + +# Event descriptions and default durations +EVENT_INFO = { + EventType.GOLDEN_HOUR: { + "name": "Золотой час", + "description": "Все очки x1.5!", + "default_duration": 45, + "points_multiplier": 1.5, + "drop_free": False, + }, + EventType.COMMON_ENEMY: { + "name": "Общий враг", + "description": "Все получают одинаковый челлендж. Первые 3 получают бонус!", + "default_duration": None, # Until all complete + "points_multiplier": 1.0, + "drop_free": False, + }, + EventType.DOUBLE_RISK: { + "name": "Двойной риск", + "description": "Дропы бесплатны, но очки x0.5", + "default_duration": 120, + "points_multiplier": 0.5, + "drop_free": True, + }, + EventType.JACKPOT: { + "name": "Джекпот", + "description": "Следующий спин — сложный челлендж с x3 очками!", + "default_duration": None, # 1 spin + "points_multiplier": 3.0, + "drop_free": False, + }, + EventType.SWAP: { + "name": "Обмен", + "description": "Можно поменяться заданием с другим участником", + "default_duration": 60, + "points_multiplier": 1.0, + "drop_free": False, + "special_action": "swap", + }, + EventType.REMATCH: { + "name": "Реванш", + "description": "Можно переделать проваленный челлендж за 50% очков", + "default_duration": 240, + "points_multiplier": 0.5, + "drop_free": False, + "special_action": "rematch", + }, +} + +# Bonus points for Common Enemy top 3 +COMMON_ENEMY_BONUSES = { + 1: 50, + 2: 30, + 3: 15, +} + + +class SwapCandidate(BaseModel): + """Participant available for assignment swap""" + participant_id: int + user: UserPublic + challenge_title: str + challenge_description: str + challenge_points: int + challenge_difficulty: str + game_title: str + + +# Two-sided swap confirmation schemas +SwapRequestStatusLiteral = Literal["pending", "accepted", "declined", "cancelled"] + + +class SwapRequestCreate(BaseModel): + """Request to swap assignment with another participant""" + target_participant_id: int + + +class SwapRequestChallengeInfo(BaseModel): + """Challenge info for swap request display""" + title: str + description: str + points: int + difficulty: str + game_title: str + + +class SwapRequestResponse(BaseModel): + """Response for a swap request""" + id: int + status: SwapRequestStatusLiteral + from_user: UserPublic + to_user: UserPublic + from_challenge: SwapRequestChallengeInfo + to_challenge: SwapRequestChallengeInfo + created_at: datetime + responded_at: datetime | None + + class Config: + from_attributes = True + + +class MySwapRequests(BaseModel): + """User's incoming and outgoing swap requests""" + incoming: list[SwapRequestResponse] + outgoing: list[SwapRequestResponse] diff --git a/backend/app/schemas/marathon.py b/backend/app/schemas/marathon.py index 2f69fc4..75b9acd 100644 --- a/backend/app/schemas/marathon.py +++ b/backend/app/schemas/marathon.py @@ -22,6 +22,7 @@ class MarathonUpdate(BaseModel): start_date: datetime | None = None is_public: bool | None = None game_proposal_mode: str | None = Field(None, pattern="^(all_participants|organizer_only)$") + auto_events_enabled: bool | None = None class ParticipantInfo(BaseModel): @@ -47,6 +48,7 @@ class MarathonResponse(MarathonBase): invite_code: str is_public: bool game_proposal_mode: str + auto_events_enabled: bool start_date: datetime | None end_date: datetime | None participants_count: int diff --git a/backend/app/services/event_scheduler.py b/backend/app/services/event_scheduler.py new file mode 100644 index 0000000..f657407 --- /dev/null +++ b/backend/app/services/event_scheduler.py @@ -0,0 +1,150 @@ +""" +Event Scheduler for automatic event launching in marathons. +""" +import asyncio +import random +from datetime import datetime, timedelta +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models import Marathon, MarathonStatus, Event, EventType +from app.services.events import EventService + + +# Configuration +CHECK_INTERVAL_SECONDS = 300 # Check every 5 minutes +EVENT_PROBABILITY = 0.1 # 10% chance per check to start an event +MIN_EVENT_GAP_HOURS = 4 # Minimum hours between events + +# Events that can be auto-triggered (excluding common_enemy which needs a challenge_id) +AUTO_EVENT_TYPES = [ + EventType.GOLDEN_HOUR, + EventType.DOUBLE_RISK, + EventType.JACKPOT, + EventType.REMATCH, +] + + +class EventScheduler: + """Background scheduler for automatic event management.""" + + def __init__(self): + self._running = False + self._task: asyncio.Task | None = None + + async def start(self, session_factory) -> None: + """Start the scheduler background task.""" + if self._running: + return + + self._running = True + self._task = asyncio.create_task(self._run_loop(session_factory)) + print("[EventScheduler] Started") + + async def stop(self) -> None: + """Stop the scheduler.""" + self._running = False + if self._task: + self._task.cancel() + try: + await self._task + except asyncio.CancelledError: + pass + print("[EventScheduler] Stopped") + + async def _run_loop(self, session_factory) -> None: + """Main scheduler loop.""" + while self._running: + try: + async with session_factory() as db: + await self._process_events(db) + except Exception as e: + print(f"[EventScheduler] Error in loop: {e}") + + await asyncio.sleep(CHECK_INTERVAL_SECONDS) + + async def _process_events(self, db: AsyncSession) -> None: + """Process events - cleanup expired and potentially start new ones.""" + # 1. Cleanup expired events + await self._cleanup_expired_events(db) + + # 2. Maybe start new events for eligible marathons + await self._maybe_start_events(db) + + async def _cleanup_expired_events(self, db: AsyncSession) -> None: + """End any events that have expired.""" + now = datetime.utcnow() + + result = await db.execute( + select(Event).where( + Event.is_active == True, + Event.end_time < now, + ) + ) + expired_events = result.scalars().all() + + for event in expired_events: + event.is_active = False + print(f"[EventScheduler] Ended expired event {event.id} ({event.type})") + + if expired_events: + await db.commit() + + async def _maybe_start_events(self, db: AsyncSession) -> None: + """Potentially start new events for eligible marathons.""" + # Get active marathons with auto_events enabled + result = await db.execute( + select(Marathon).where( + Marathon.status == MarathonStatus.ACTIVE.value, + Marathon.auto_events_enabled == True, + ) + ) + marathons = result.scalars().all() + + event_service = EventService() + + for marathon in marathons: + # Skip if random chance doesn't hit + if random.random() > EVENT_PROBABILITY: + continue + + # Check if there's already an active event + active_event = await event_service.get_active_event(db, marathon.id) + if active_event: + continue + + # Check if enough time has passed since last event + result = await db.execute( + select(Event) + .where(Event.marathon_id == marathon.id) + .order_by(Event.end_time.desc()) + .limit(1) + ) + last_event = result.scalar_one_or_none() + + if last_event: + time_since_last = datetime.utcnow() - last_event.end_time + if time_since_last < timedelta(hours=MIN_EVENT_GAP_HOURS): + continue + + # Start a random event + event_type = random.choice(AUTO_EVENT_TYPES) + + try: + event = await event_service.start_event( + db=db, + marathon_id=marathon.id, + event_type=event_type.value, + created_by_id=None, # null = auto-started + ) + print( + f"[EventScheduler] Auto-started {event_type.value} for marathon {marathon.id}" + ) + except Exception as e: + print( + f"[EventScheduler] Failed to start event for marathon {marathon.id}: {e}" + ) + + +# Global scheduler instance +event_scheduler = EventScheduler() diff --git a/backend/app/services/events.py b/backend/app/services/events.py new file mode 100644 index 0000000..41c1f67 --- /dev/null +++ b/backend/app/services/events.py @@ -0,0 +1,227 @@ +from datetime import datetime, timedelta +from sqlalchemy import select +from sqlalchemy.orm import selectinload +from sqlalchemy.orm.attributes import flag_modified +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models import Event, EventType, Marathon, Challenge, Difficulty +from app.schemas.event import EventEffects, EVENT_INFO, COMMON_ENEMY_BONUSES + + +class EventService: + """Service for managing marathon events""" + + async def get_active_event(self, db: AsyncSession, marathon_id: int) -> Event | None: + """Get currently active event for marathon""" + now = datetime.utcnow() + result = await db.execute( + select(Event) + .options(selectinload(Event.created_by)) + .where( + Event.marathon_id == marathon_id, + Event.is_active == True, + Event.start_time <= now, + ) + .order_by(Event.start_time.desc()) + ) + event = result.scalar_one_or_none() + + # Check if event has expired + if event and event.end_time and event.end_time < now: + await self.end_event(db, event.id) + return None + + return event + + async def can_start_event(self, db: AsyncSession, marathon_id: int) -> bool: + """Check if we can start a new event (no active event exists)""" + active = await self.get_active_event(db, marathon_id) + return active is None + + async def start_event( + self, + db: AsyncSession, + marathon_id: int, + event_type: str, + created_by_id: int | None = None, + duration_minutes: int | None = None, + challenge_id: int | None = None, + ) -> Event: + """Start a new event""" + # Check no active event + if not await self.can_start_event(db, marathon_id): + raise ValueError("An event is already active") + + # Get default duration if not provided + event_info = EVENT_INFO.get(EventType(event_type), {}) + if duration_minutes is None: + duration_minutes = event_info.get("default_duration") + + now = datetime.utcnow() + end_time = now + timedelta(minutes=duration_minutes) if duration_minutes else None + + # Build event data + data = {} + if event_type == EventType.COMMON_ENEMY.value and challenge_id: + data["challenge_id"] = challenge_id + data["completions"] = [] # Track who completed and when + + event = Event( + marathon_id=marathon_id, + type=event_type, + start_time=now, + end_time=end_time, + is_active=True, + created_by_id=created_by_id, + data=data if data else None, + ) + db.add(event) + await db.commit() + await db.refresh(event) + + # Load created_by relationship + if created_by_id: + await db.refresh(event, ["created_by"]) + + return event + + async def end_event(self, db: AsyncSession, event_id: int) -> None: + """End an event""" + result = await db.execute(select(Event).where(Event.id == event_id)) + event = result.scalar_one_or_none() + if event: + event.is_active = False + if not event.end_time: + event.end_time = datetime.utcnow() + await db.commit() + + async def consume_jackpot(self, db: AsyncSession, event_id: int) -> None: + """Consume jackpot event after one spin""" + await self.end_event(db, event_id) + + def get_event_effects(self, event: Event | None) -> EventEffects: + """Get effects of an event""" + if not event: + return EventEffects(description="Нет активного события") + + event_info = EVENT_INFO.get(EventType(event.type), {}) + + return EventEffects( + points_multiplier=event_info.get("points_multiplier", 1.0), + drop_free=event_info.get("drop_free", False), + special_action=event_info.get("special_action"), + description=event_info.get("description", ""), + ) + + async def get_random_hard_challenge( + self, + db: AsyncSession, + marathon_id: int + ) -> Challenge | None: + """Get a random hard challenge for jackpot event""" + result = await db.execute( + select(Challenge) + .join(Challenge.game) + .where( + Challenge.game.has(marathon_id=marathon_id), + Challenge.difficulty == Difficulty.HARD.value, + ) + ) + challenges = result.scalars().all() + if not challenges: + # Fallback to any challenge + result = await db.execute( + select(Challenge) + .join(Challenge.game) + .where(Challenge.game.has(marathon_id=marathon_id)) + ) + challenges = result.scalars().all() + + if challenges: + import random + return random.choice(challenges) + return None + + async def record_common_enemy_completion( + self, + db: AsyncSession, + event: Event, + participant_id: int, + user_id: int, + ) -> tuple[int, bool, list[dict] | None]: + """ + Record completion for common enemy event. + Returns: (bonus_points, event_closed, winners_list) + - bonus_points: bonus for this completion (top 3 get bonuses) + - event_closed: True if event was auto-closed (3 completions reached) + - winners_list: list of winners if event closed, None otherwise + """ + if event.type != EventType.COMMON_ENEMY.value: + return 0, False, None + + data = event.data or {} + completions = data.get("completions", []) + + # Check if already completed + if any(c["participant_id"] == participant_id for c in completions): + return 0, False, None + + # Add completion + rank = len(completions) + 1 + completions.append({ + "participant_id": participant_id, + "user_id": user_id, + "completed_at": datetime.utcnow().isoformat(), + "rank": rank, + }) + + # Update event data - need to flag_modified for SQLAlchemy to detect JSON changes + event.data = {**data, "completions": completions} + flag_modified(event, "data") + + bonus = COMMON_ENEMY_BONUSES.get(rank, 0) + + # Auto-close event when 3 players completed + event_closed = False + winners_list = None + if rank >= 3: + event.is_active = False + event.end_time = datetime.utcnow() + event_closed = True + winners_list = completions[:3] # Top 3 + + await db.commit() + + return bonus, event_closed, winners_list + + async def get_common_enemy_challenge( + self, + db: AsyncSession, + event: Event + ) -> Challenge | None: + """Get the challenge for common enemy event""" + if event.type != EventType.COMMON_ENEMY.value: + return None + + data = event.data or {} + challenge_id = data.get("challenge_id") + if not challenge_id: + return None + + result = await db.execute( + select(Challenge) + .options(selectinload(Challenge.game)) + .where(Challenge.id == challenge_id) + ) + return result.scalar_one_or_none() + + def get_time_remaining(self, event: Event | None) -> int | None: + """Get remaining time in seconds for an event""" + if not event or not event.end_time: + return None + + remaining = (event.end_time - datetime.utcnow()).total_seconds() + return max(0, int(remaining)) + + +event_service = EventService() diff --git a/backend/app/services/points.py b/backend/app/services/points.py index 287e5c4..1f88f9f 100644 --- a/backend/app/services/points.py +++ b/backend/app/services/points.py @@ -1,3 +1,6 @@ +from app.models import Event, EventType + + class PointsService: """Service for calculating points and penalties""" @@ -17,39 +20,77 @@ class PointsService: } MAX_DROP_PENALTY = 50 + # Event point multipliers + EVENT_MULTIPLIERS = { + EventType.GOLDEN_HOUR.value: 1.5, + EventType.DOUBLE_RISK.value: 0.5, + EventType.JACKPOT.value: 3.0, + EventType.REMATCH.value: 0.5, + } + def calculate_completion_points( self, base_points: int, - current_streak: int - ) -> tuple[int, int]: + current_streak: int, + event: Event | None = None, + ) -> tuple[int, int, int]: """ Calculate points earned for completing a challenge. Args: base_points: Base points for the challenge current_streak: Current streak before this completion + event: Active event (optional) Returns: - Tuple of (total_points, streak_bonus) + Tuple of (total_points, streak_bonus, event_bonus) """ - multiplier = self.STREAK_MULTIPLIERS.get( + # Apply event multiplier first + event_multiplier = 1.0 + if event: + event_multiplier = self.EVENT_MULTIPLIERS.get(event.type, 1.0) + + adjusted_base = int(base_points * event_multiplier) + event_bonus = adjusted_base - base_points + + # Then apply streak bonus + streak_multiplier = self.STREAK_MULTIPLIERS.get( current_streak, self.MAX_STREAK_MULTIPLIER ) - bonus = int(base_points * multiplier) - return base_points + bonus, bonus + streak_bonus = int(adjusted_base * streak_multiplier) - def calculate_drop_penalty(self, consecutive_drops: int) -> int: + total_points = adjusted_base + streak_bonus + return total_points, streak_bonus, event_bonus + + def calculate_drop_penalty( + self, + consecutive_drops: int, + event: Event | None = None + ) -> int: """ Calculate penalty for dropping a challenge. Args: consecutive_drops: Number of drops since last completion + event: Active event (optional) Returns: Penalty points to subtract """ + # Double risk event = free drops + if event and event.type == EventType.DOUBLE_RISK.value: + return 0 + return self.DROP_PENALTIES.get( consecutive_drops, self.MAX_DROP_PENALTY ) + + def apply_event_multiplier(self, base_points: int, event: Event | None) -> int: + """Apply event multiplier to points""" + if not event: + return base_points + + multiplier = self.EVENT_MULTIPLIERS.get(event.type, 1.0) + return int(base_points * multiplier) diff --git a/frontend/Dockerfile b/frontend/Dockerfile index f107039..b1ecbe0 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -3,8 +3,8 @@ FROM node:20-alpine as build WORKDIR /app # Install dependencies -COPY package.json ./ -RUN npm install +COPY package.json package-lock.json ./ +RUN npm ci --network-timeout 300000 # Copy source COPY . . diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..ebbac89 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,4253 @@ +{ + "name": "game-marathon-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "game-marathon-frontend", + "version": "1.0.0", + "dependencies": { + "@hookform/resolvers": "^3.3.2", + "axios": "^1.6.2", + "clsx": "^2.0.0", + "date-fns": "^3.0.6", + "framer-motion": "^10.16.16", + "lucide-react": "^0.303.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-hook-form": "^7.49.2", + "react-router-dom": "^6.21.0", + "tailwind-merge": "^2.2.0", + "zod": "^3.22.4", + "zustand": "^4.4.7" + }, + "devDependencies": { + "@types/react": "^18.2.45", + "@types/react-dom": "^18.2.18", + "@typescript-eslint/eslint-plugin": "^6.15.0", + "@typescript-eslint/parser": "^6.15.0", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "eslint": "^8.56.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "postcss": "^8.4.32", + "tailwindcss": "^3.4.0", + "typescript": "^5.3.3", + "vite": "^5.0.10" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "optional": true, + "dependencies": { + "@emotion/memoize": "0.7.4" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "optional": true + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@hookform/resolvers": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz", + "integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "devOptional": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/autoprefixer": { + "version": "10.4.23", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.7", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.7.tgz", + "integrity": "sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001760", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.25", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.25.tgz", + "integrity": "sha512-dRUD2LOdEqI4zXHqbQ442blQAzdSuShAaiSq5Vtyy6LT08YUf0oOjBDo4VPx0dCPgiPWh1WB4dtbLOd0kOlDPQ==", + "dev": true, + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.18.0.tgz", + "integrity": "sha512-oGlDh1Q1XqYPksuTD/usb0I70hq95OUzmL9+6Zd+Hs4XV0oaISBa/UUMSjYiq6m8EUF32132mOJ8xVZS+I0S6w==", + "dependencies": { + "tslib": "^2.4.0" + }, + "optionalDependencies": { + "@emotion/is-prop-valid": "^0.8.2" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.303.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.303.0.tgz", + "integrity": "sha512-B0B9T3dLEFBYPCUlnUS1mvAhW1craSbF9HO+JfBjAtpFUJ7gMIqmEwNSclikY3RiN2OnCkj/V1ReAQpaHae8Bg==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-hook-form": { + "version": "7.68.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz", + "integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz", + "integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==", + "dependencies": { + "@remix-run/router": "1.23.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz", + "integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==", + "dependencies": { + "@remix-run/router": "1.23.1", + "react-router": "6.30.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", + "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/frontend/src/api/challenges.ts b/frontend/src/api/challenges.ts new file mode 100644 index 0000000..93a63ed --- /dev/null +++ b/frontend/src/api/challenges.ts @@ -0,0 +1,9 @@ +import client from './client' +import type { Challenge } from '@/types' + +export const challengesApi = { + list: async (marathonId: number): Promise => { + const response = await client.get(`/marathons/${marathonId}/challenges`) + return response.data + }, +} diff --git a/frontend/src/api/events.ts b/frontend/src/api/events.ts new file mode 100644 index 0000000..ba96756 --- /dev/null +++ b/frontend/src/api/events.ts @@ -0,0 +1,67 @@ +import client from './client' +import type { ActiveEvent, MarathonEvent, EventCreate, DroppedAssignment, SwapCandidate, SwapRequestItem, MySwapRequests, CommonEnemyLeaderboardEntry } from '@/types' + +export const eventsApi = { + getActive: async (marathonId: number): Promise => { + const response = await client.get(`/marathons/${marathonId}/event`) + return response.data + }, + + list: async (marathonId: number): Promise => { + const response = await client.get(`/marathons/${marathonId}/events`) + return response.data + }, + + start: async (marathonId: number, data: EventCreate): Promise => { + const response = await client.post(`/marathons/${marathonId}/events`, data) + return response.data + }, + + stop: async (marathonId: number): Promise => { + await client.delete(`/marathons/${marathonId}/event`) + }, + + // Swap requests (two-sided confirmation) + createSwapRequest: async (marathonId: number, targetParticipantId: number): Promise => { + const response = await client.post(`/marathons/${marathonId}/swap-requests`, { + target_participant_id: targetParticipantId, + }) + return response.data + }, + + getSwapRequests: async (marathonId: number): Promise => { + const response = await client.get(`/marathons/${marathonId}/swap-requests`) + return response.data + }, + + acceptSwapRequest: async (marathonId: number, requestId: number): Promise => { + await client.post(`/marathons/${marathonId}/swap-requests/${requestId}/accept`) + }, + + declineSwapRequest: async (marathonId: number, requestId: number): Promise => { + await client.post(`/marathons/${marathonId}/swap-requests/${requestId}/decline`) + }, + + cancelSwapRequest: async (marathonId: number, requestId: number): Promise => { + await client.delete(`/marathons/${marathonId}/swap-requests/${requestId}`) + }, + + rematch: async (marathonId: number, assignmentId: number): Promise => { + await client.post(`/marathons/${marathonId}/rematch/${assignmentId}`) + }, + + getDroppedAssignments: async (marathonId: number): Promise => { + const response = await client.get(`/marathons/${marathonId}/dropped-assignments`) + return response.data + }, + + getSwapCandidates: async (marathonId: number): Promise => { + const response = await client.get(`/marathons/${marathonId}/swap-candidates`) + return response.data + }, + + getCommonEnemyLeaderboard: async (marathonId: number): Promise => { + const response = await client.get(`/marathons/${marathonId}/common-enemy-leaderboard`) + return response.data + }, +} diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index e60d193..7ef67dc 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -4,3 +4,5 @@ export { gamesApi } from './games' export { wheelApi } from './wheel' export { feedApi } from './feed' export { adminApi } from './admin' +export { eventsApi } from './events' +export { challengesApi } from './challenges' diff --git a/frontend/src/components/EventBanner.tsx b/frontend/src/components/EventBanner.tsx new file mode 100644 index 0000000..0be17d4 --- /dev/null +++ b/frontend/src/components/EventBanner.tsx @@ -0,0 +1,110 @@ +import { useState, useEffect } from 'react' +import { Zap, Users, Shield, Gift, ArrowLeftRight, RotateCcw, Clock } from 'lucide-react' +import type { ActiveEvent, EventType } from '@/types' +import { EVENT_INFO } from '@/types' + +interface EventBannerProps { + activeEvent: ActiveEvent + onRefresh?: () => void +} + +const EVENT_ICONS: Record = { + golden_hour: , + common_enemy: , + double_risk: , + jackpot: , + swap: , + rematch: , +} + +const EVENT_COLORS: Record = { + golden_hour: 'from-yellow-500/20 to-yellow-600/20 border-yellow-500/50 text-yellow-400', + common_enemy: 'from-red-500/20 to-red-600/20 border-red-500/50 text-red-400', + double_risk: 'from-purple-500/20 to-purple-600/20 border-purple-500/50 text-purple-400', + jackpot: 'from-green-500/20 to-green-600/20 border-green-500/50 text-green-400', + swap: 'from-blue-500/20 to-blue-600/20 border-blue-500/50 text-blue-400', + rematch: 'from-orange-500/20 to-orange-600/20 border-orange-500/50 text-orange-400', +} + +function formatTime(seconds: number): string { + const hours = Math.floor(seconds / 3600) + const minutes = Math.floor((seconds % 3600) / 60) + const secs = seconds % 60 + + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}` + } + return `${minutes}:${secs.toString().padStart(2, '0')}` +} + +export function EventBanner({ activeEvent, onRefresh }: EventBannerProps) { + const [timeRemaining, setTimeRemaining] = useState(activeEvent.time_remaining_seconds) + + useEffect(() => { + setTimeRemaining(activeEvent.time_remaining_seconds) + }, [activeEvent.time_remaining_seconds]) + + useEffect(() => { + if (timeRemaining === null || timeRemaining <= 0) return + + const timer = setInterval(() => { + setTimeRemaining((prev) => { + if (prev === null || prev <= 0) { + clearInterval(timer) + onRefresh?.() + return 0 + } + return prev - 1 + }) + }, 1000) + + return () => clearInterval(timer) + }, [timeRemaining, onRefresh]) + + if (!activeEvent.event) { + return null + } + + const event = activeEvent.event + const info = EVENT_INFO[event.type] + const icon = EVENT_ICONS[event.type] + const colorClass = EVENT_COLORS[event.type] + + return ( +
+ {/* Animated background effect */} +
+ +
+
+
+ {icon} +
+
+

{info.name}

+

{info.description}

+
+
+ + {timeRemaining !== null && timeRemaining > 0 && ( +
+ + {formatTime(timeRemaining)} +
+ )} + + {activeEvent.effects.points_multiplier !== 1.0 && ( +
+ x{activeEvent.effects.points_multiplier} +
+ )} +
+
+ ) +} diff --git a/frontend/src/components/EventControl.tsx b/frontend/src/components/EventControl.tsx new file mode 100644 index 0000000..008f00a --- /dev/null +++ b/frontend/src/components/EventControl.tsx @@ -0,0 +1,161 @@ +import { useState } from 'react' +import { Zap, Users, Shield, Gift, ArrowLeftRight, RotateCcw, Play, Square } from 'lucide-react' +import { Button } from '@/components/ui' +import { eventsApi } from '@/api' +import type { ActiveEvent, EventType, Challenge } from '@/types' +import { EVENT_INFO } from '@/types' + +interface EventControlProps { + marathonId: number + activeEvent: ActiveEvent + challenges?: Challenge[] + onEventChange: () => void +} + +const EVENT_TYPES: EventType[] = [ + 'golden_hour', + 'double_risk', + 'jackpot', + 'swap', + 'rematch', + 'common_enemy', +] + +const EVENT_ICONS: Record = { + golden_hour: , + common_enemy: , + double_risk: , + jackpot: , + swap: , + rematch: , +} + +export function EventControl({ + marathonId, + activeEvent, + challenges, + onEventChange, +}: EventControlProps) { + const [selectedType, setSelectedType] = useState('golden_hour') + const [selectedChallengeId, setSelectedChallengeId] = useState(null) + const [isStarting, setIsStarting] = useState(false) + const [isStopping, setIsStopping] = useState(false) + + const handleStart = async () => { + if (selectedType === 'common_enemy' && !selectedChallengeId) { + alert('Выберите челлендж для события "Общий враг"') + return + } + + setIsStarting(true) + try { + await eventsApi.start(marathonId, { + type: selectedType, + challenge_id: selectedType === 'common_enemy' ? selectedChallengeId ?? undefined : undefined, + }) + onEventChange() + } catch (error) { + console.error('Failed to start event:', error) + alert('Не удалось запустить событие') + } finally { + setIsStarting(false) + } + } + + const handleStop = async () => { + if (!confirm('Остановить событие досрочно?')) return + + setIsStopping(true) + try { + await eventsApi.stop(marathonId) + onEventChange() + } catch (error) { + console.error('Failed to stop event:', error) + } finally { + setIsStopping(false) + } + } + + if (activeEvent.event) { + return ( +
+
+
+ {EVENT_ICONS[activeEvent.event.type]} + + Активно: {EVENT_INFO[activeEvent.event.type].name} + +
+ +
+
+ ) + } + + return ( +
+

Запустить событие

+ +
+ {EVENT_TYPES.map((type) => ( + + ))} +
+ + {selectedType === 'common_enemy' && challenges && challenges.length > 0 && ( +
+ + +
+ )} + + +
+ ) +} diff --git a/frontend/src/pages/MarathonPage.tsx b/frontend/src/pages/MarathonPage.tsx index 5b400ce..f9d0790 100644 --- a/frontend/src/pages/MarathonPage.tsx +++ b/frontend/src/pages/MarathonPage.tsx @@ -1,10 +1,12 @@ import { useState, useEffect } from 'react' import { useParams, useNavigate, Link } from 'react-router-dom' -import { marathonsApi } from '@/api' -import type { Marathon } from '@/types' +import { marathonsApi, eventsApi, challengesApi } from '@/api' +import type { Marathon, ActiveEvent, Challenge } from '@/types' import { Button, Card, CardContent } from '@/components/ui' import { useAuthStore } from '@/store/auth' -import { Users, Calendar, Trophy, Play, Settings, Copy, Check, Loader2, Trash2, Globe, Lock, CalendarCheck, UserPlus, Gamepad2, ArrowLeft } from 'lucide-react' +import { EventBanner } from '@/components/EventBanner' +import { EventControl } from '@/components/EventControl' +import { Users, Calendar, Trophy, Play, Settings, Copy, Check, Loader2, Trash2, Globe, Lock, CalendarCheck, UserPlus, Gamepad2, ArrowLeft, Zap } from 'lucide-react' import { format } from 'date-fns' export function MarathonPage() { @@ -12,10 +14,13 @@ export function MarathonPage() { const navigate = useNavigate() const user = useAuthStore((state) => state.user) const [marathon, setMarathon] = useState(null) + const [activeEvent, setActiveEvent] = useState(null) + const [challenges, setChallenges] = useState([]) const [isLoading, setIsLoading] = useState(true) const [copied, setCopied] = useState(false) const [isDeleting, setIsDeleting] = useState(false) const [isJoining, setIsJoining] = useState(false) + const [showEventControl, setShowEventControl] = useState(false) useEffect(() => { loadMarathon() @@ -26,6 +31,22 @@ export function MarathonPage() { try { const data = await marathonsApi.get(parseInt(id)) setMarathon(data) + + // Load event data if marathon is active + if (data.status === 'active' && data.my_participation) { + const eventData = await eventsApi.getActive(parseInt(id)) + setActiveEvent(eventData) + + // Load challenges for event control if organizer + if (data.my_participation.role === 'organizer') { + try { + const challengesData = await challengesApi.list(parseInt(id)) + setChallenges(challengesData) + } catch { + // Ignore if no challenges + } + } + } } catch (error) { console.error('Failed to load marathon:', error) navigate('/marathons') @@ -34,6 +55,16 @@ export function MarathonPage() { } } + const refreshEvent = async () => { + if (!id) return + try { + const eventData = await eventsApi.getActive(parseInt(id)) + setActiveEvent(eventData) + } catch (error) { + console.error('Failed to refresh event:', error) + } + } + const getInviteLink = () => { if (!marathon) return '' return `${window.location.origin}/invite/${marathon.invite_code}` @@ -234,6 +265,42 @@ export function MarathonPage() {
+ {/* Active event banner */} + {marathon.status === 'active' && activeEvent?.event && ( +
+ +
+ )} + + {/* Event control for organizers */} + {marathon.status === 'active' && isOrganizer && ( + + +
+

+ + Управление событиями +

+ +
+ {showEventControl && activeEvent && ( + + )} +
+
+ )} + {/* Invite link */} {marathon.status !== 'finished' && ( diff --git a/frontend/src/pages/PlayPage.tsx b/frontend/src/pages/PlayPage.tsx index ec77c38..21f874f 100644 --- a/frontend/src/pages/PlayPage.tsx +++ b/frontend/src/pages/PlayPage.tsx @@ -1,10 +1,11 @@ import { useState, useEffect, useRef } from 'react' -import { useParams } from 'react-router-dom' -import { marathonsApi, wheelApi, gamesApi } from '@/api' -import type { Marathon, Assignment, SpinResult, Game } from '@/types' +import { useParams, Link } from 'react-router-dom' +import { marathonsApi, wheelApi, gamesApi, eventsApi } from '@/api' +import type { Marathon, Assignment, SpinResult, Game, ActiveEvent, DroppedAssignment, SwapCandidate, MySwapRequests, CommonEnemyLeaderboardEntry } from '@/types' import { Button, Card, CardContent } from '@/components/ui' import { SpinWheel } from '@/components/SpinWheel' -import { Loader2, Upload, X } from 'lucide-react' +import { EventBanner } from '@/components/EventBanner' +import { Loader2, Upload, X, RotateCcw, ArrowLeftRight, Check, XCircle, Clock, Send, Trophy, Users, ArrowLeft } from 'lucide-react' export function PlayPage() { const { id } = useParams<{ id: string }>() @@ -13,6 +14,7 @@ export function PlayPage() { const [currentAssignment, setCurrentAssignment] = useState(null) const [spinResult, setSpinResult] = useState(null) const [games, setGames] = useState([]) + const [activeEvent, setActiveEvent] = useState(null) const [isLoading, setIsLoading] = useState(true) // Complete state @@ -24,23 +26,113 @@ export function PlayPage() { // Drop state const [isDropping, setIsDropping] = useState(false) + // Rematch state + const [droppedAssignments, setDroppedAssignments] = useState([]) + const [isRematchLoading, setIsRematchLoading] = useState(false) + const [rematchingId, setRematchingId] = useState(null) + + // Swap state + const [swapCandidates, setSwapCandidates] = useState([]) + const [swapRequests, setSwapRequests] = useState({ incoming: [], outgoing: [] }) + const [isSwapLoading, setIsSwapLoading] = useState(false) + const [sendingRequestTo, setSendingRequestTo] = useState(null) + const [processingRequestId, setProcessingRequestId] = useState(null) + + // Common Enemy leaderboard state + const [commonEnemyLeaderboard, setCommonEnemyLeaderboard] = useState([]) + const fileInputRef = useRef(null) useEffect(() => { loadData() }, [id]) + // Load dropped assignments when rematch event is active + useEffect(() => { + if (activeEvent?.event?.type === 'rematch' && !currentAssignment) { + loadDroppedAssignments() + } + }, [activeEvent?.event?.type, currentAssignment]) + + // Load swap candidates and requests when swap event is active + useEffect(() => { + if (activeEvent?.event?.type === 'swap') { + loadSwapRequests() + if (currentAssignment) { + loadSwapCandidates() + } + } + }, [activeEvent?.event?.type, currentAssignment]) + + // Load common enemy leaderboard when common_enemy event is active + useEffect(() => { + if (activeEvent?.event?.type === 'common_enemy') { + loadCommonEnemyLeaderboard() + // Poll for updates every 10 seconds + const interval = setInterval(loadCommonEnemyLeaderboard, 10000) + return () => clearInterval(interval) + } + }, [activeEvent?.event?.type]) + + const loadDroppedAssignments = async () => { + if (!id) return + setIsRematchLoading(true) + try { + const dropped = await eventsApi.getDroppedAssignments(parseInt(id)) + setDroppedAssignments(dropped) + } catch (error) { + console.error('Failed to load dropped assignments:', error) + } finally { + setIsRematchLoading(false) + } + } + + const loadSwapCandidates = async () => { + if (!id) return + setIsSwapLoading(true) + try { + const candidates = await eventsApi.getSwapCandidates(parseInt(id)) + setSwapCandidates(candidates) + } catch (error) { + console.error('Failed to load swap candidates:', error) + } finally { + setIsSwapLoading(false) + } + } + + const loadSwapRequests = async () => { + if (!id) return + try { + const requests = await eventsApi.getSwapRequests(parseInt(id)) + setSwapRequests(requests) + } catch (error) { + console.error('Failed to load swap requests:', error) + } + } + + const loadCommonEnemyLeaderboard = async () => { + if (!id) return + try { + const leaderboard = await eventsApi.getCommonEnemyLeaderboard(parseInt(id)) + setCommonEnemyLeaderboard(leaderboard) + } catch (error) { + console.error('Failed to load common enemy leaderboard:', error) + } + } + const loadData = async () => { if (!id) return try { - const [marathonData, assignment, gamesData] = await Promise.all([ + const [marathonData, assignment, gamesData, eventData] = await Promise.all([ marathonsApi.get(parseInt(id)), wheelApi.getCurrentAssignment(parseInt(id)), gamesApi.list(parseInt(id), 'approved'), + eventsApi.getActive(parseInt(id)), ]) setMarathon(marathonData) setCurrentAssignment(assignment) setGames(gamesData) + setActiveEvent(eventData) } catch (error) { console.error('Failed to load data:', error) } finally { @@ -48,6 +140,16 @@ export function PlayPage() { } } + const refreshEvent = async () => { + if (!id) return + try { + const eventData = await eventsApi.getActive(parseInt(id)) + setActiveEvent(eventData) + } catch (error) { + console.error('Failed to refresh event:', error) + } + } + const handleSpin = async (): Promise => { if (!id) return null @@ -122,6 +224,92 @@ export function PlayPage() { } } + const handleRematch = async (assignmentId: number) => { + if (!id) return + + if (!confirm('Начать реванш? Вы получите 50% от обычных очков за выполнение.')) return + + setRematchingId(assignmentId) + try { + await eventsApi.rematch(parseInt(id), assignmentId) + alert('Реванш начат! Выполните задание за 50% очков.') + await loadData() + } catch (err: unknown) { + const error = err as { response?: { data?: { detail?: string } } } + alert(error.response?.data?.detail || 'Не удалось начать реванш') + } finally { + setRematchingId(null) + } + } + + const handleSendSwapRequest = async (participantId: number, participantName: string, theirChallenge: string) => { + if (!id) return + + if (!confirm(`Отправить запрос на обмен с ${participantName}?\n\nВы предлагаете обменяться на: "${theirChallenge}"\n\n${participantName} должен будет подтвердить обмен.`)) return + + setSendingRequestTo(participantId) + try { + await eventsApi.createSwapRequest(parseInt(id), participantId) + alert('Запрос на обмен отправлен! Ожидайте подтверждения.') + await loadSwapRequests() + await loadSwapCandidates() + } catch (err: unknown) { + const error = err as { response?: { data?: { detail?: string } } } + alert(error.response?.data?.detail || 'Не удалось отправить запрос') + } finally { + setSendingRequestTo(null) + } + } + + const handleAcceptSwapRequest = async (requestId: number) => { + if (!id) return + + if (!confirm('Принять обмен? Задания будут обменяны сразу после подтверждения.')) return + + setProcessingRequestId(requestId) + try { + await eventsApi.acceptSwapRequest(parseInt(id), requestId) + alert('Обмен выполнен!') + await loadData() + } catch (err: unknown) { + const error = err as { response?: { data?: { detail?: string } } } + alert(error.response?.data?.detail || 'Не удалось выполнить обмен') + } finally { + setProcessingRequestId(null) + } + } + + const handleDeclineSwapRequest = async (requestId: number) => { + if (!id) return + + setProcessingRequestId(requestId) + try { + await eventsApi.declineSwapRequest(parseInt(id), requestId) + await loadSwapRequests() + } catch (err: unknown) { + const error = err as { response?: { data?: { detail?: string } } } + alert(error.response?.data?.detail || 'Не удалось отклонить запрос') + } finally { + setProcessingRequestId(null) + } + } + + const handleCancelSwapRequest = async (requestId: number) => { + if (!id) return + + setProcessingRequestId(requestId) + try { + await eventsApi.cancelSwapRequest(parseInt(id), requestId) + await loadSwapRequests() + await loadSwapCandidates() + } catch (err: unknown) { + const error = err as { response?: { data?: { detail?: string } } } + alert(error.response?.data?.detail || 'Не удалось отменить запрос') + } finally { + setProcessingRequestId(null) + } + } + if (isLoading) { return (
@@ -138,8 +326,14 @@ export function PlayPage() { return (
+ {/* Back button */} + + + К марафону + + {/* Header stats */} -
+
@@ -166,25 +360,144 @@ export function PlayPage() {
- {/* No active assignment - show spin wheel */} - {!currentAssignment && ( - - -

Крутите колесо!

-

- Получите случайную игру и задание для выполнения -

- + {/* Active event banner */} + {activeEvent?.event && ( +
+ +
+ )} + + {/* Common Enemy Leaderboard */} + {activeEvent?.event?.type === 'common_enemy' && ( + + +
+ +

Выполнили челлендж

+ {commonEnemyLeaderboard.length > 0 && ( + + {commonEnemyLeaderboard.length} чел. + + )} +
+ + {commonEnemyLeaderboard.length === 0 ? ( +
+ Пока никто не выполнил. Будь первым! +
+ ) : ( +
+ {commonEnemyLeaderboard.map((entry) => ( +
+
+ {entry.rank && entry.rank <= 3 ? ( + + ) : ( + entry.rank + )} +
+
+

{entry.user.nickname}

+
+ {entry.bonus_points > 0 && ( + + +{entry.bonus_points} бонус + + )} +
+ ))} +
+ )}
)} + {/* No active assignment - show spin wheel */} + {!currentAssignment && ( + <> + + +

Крутите колесо!

+

+ Получите случайную игру и задание для выполнения +

+ +
+
+ + {/* Rematch section - show during rematch event */} + {activeEvent?.event?.type === 'rematch' && droppedAssignments.length > 0 && ( + + +
+ +

Реванш

+
+

+ Во время события "Реванш" вы можете повторить пропущенные задания за 50% очков +

+ + {isRematchLoading ? ( +
+ +
+ ) : ( +
+ {droppedAssignments.map((dropped) => ( +
+
+

+ {dropped.challenge.title} +

+

+ {dropped.challenge.game.title} • {dropped.challenge.points * 0.5} очков +

+
+ +
+ ))} +
+ )} +
+
+ )} + + )} + {/* Active assignment */} {currentAssignment && ( + <>
@@ -315,6 +628,184 @@ export function PlayPage() {
+ + {/* Swap section - show during swap event when user has active assignment */} + {activeEvent?.event?.type === 'swap' && ( + + +
+ +

Обмен заданиями

+
+

+ Обмен требует подтверждения с обеих сторон +

+ + {/* Incoming swap requests */} + {swapRequests.incoming.length > 0 && ( +
+

+ + Входящие запросы ({swapRequests.incoming.length}) +

+
+ {swapRequests.incoming.map((request) => ( +
+
+
+

+ {request.from_user.nickname} предлагает обмен +

+

+ Вы получите: {request.from_challenge.title} +

+

+ {request.from_challenge.game_title} • {request.from_challenge.points} очков +

+

+ Взамен на: {request.to_challenge.title} +

+
+
+ + +
+
+
+ ))} +
+
+ )} + + {/* Outgoing swap requests */} + {swapRequests.outgoing.length > 0 && ( +
+

+ + Отправленные запросы ({swapRequests.outgoing.length}) +

+
+ {swapRequests.outgoing.map((request) => ( +
+
+
+

+ Запрос к {request.to_user.nickname} +

+

+ Вы получите: {request.to_challenge.title} +

+

+ {request.to_challenge.game_title} • {request.to_challenge.points} очков +

+

+ Ожидание подтверждения... +

+
+ +
+
+ ))} +
+
+ )} + + {/* Swap candidates */} +
+

+ Доступные для обмена +

+ {isSwapLoading ? ( +
+ +
+ ) : swapCandidates.filter(c => + !swapRequests.outgoing.some(r => r.to_user.id === c.user.id) && + !swapRequests.incoming.some(r => r.from_user.id === c.user.id) + ).length === 0 ? ( +
+ Нет участников для обмена +
+ ) : ( +
+ {swapCandidates + .filter(c => + !swapRequests.outgoing.some(r => r.to_user.id === c.user.id) && + !swapRequests.incoming.some(r => r.from_user.id === c.user.id) + ) + .map((candidate) => ( +
+
+
+

+ {candidate.user.nickname} +

+

+ {candidate.challenge_title} +

+

+ {candidate.game_title} • {candidate.challenge_points} очков • {candidate.challenge_difficulty} +

+
+ +
+
+ ))} +
+ )} +
+
+
+ )} + )}
) diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index ccfc140..f840b8b 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -43,6 +43,7 @@ export interface Marathon { invite_code: string is_public: boolean game_proposal_mode: GameProposalMode + auto_events_enabled: boolean start_date: string | null end_date: string | null participants_count: number @@ -192,6 +193,58 @@ export interface DropResult { new_drop_count: number } +export interface DroppedAssignment { + id: number + challenge: Challenge + dropped_at: string +} + +export interface SwapCandidate { + participant_id: number + user: User + challenge_title: string + challenge_description: string + challenge_points: number + challenge_difficulty: Difficulty + game_title: string +} + +// Two-sided swap confirmation types +export type SwapRequestStatus = 'pending' | 'accepted' | 'declined' | 'cancelled' + +export interface SwapRequestChallengeInfo { + title: string + description: string + points: number + difficulty: string + game_title: string +} + +export interface SwapRequestItem { + id: number + status: SwapRequestStatus + from_user: User + to_user: User + from_challenge: SwapRequestChallengeInfo + to_challenge: SwapRequestChallengeInfo + created_at: string + responded_at: string | null +} + +export interface MySwapRequests { + incoming: SwapRequestItem[] + outgoing: SwapRequestItem[] +} + +// Common Enemy leaderboard +export interface CommonEnemyLeaderboardEntry { + participant_id: number + user: User + completed_at: string | null + rank: number | null + bonus_points: number +} + // Activity types export type ActivityType = | 'join' @@ -218,6 +271,78 @@ export interface FeedResponse { has_more: boolean } +// Event types +export type EventType = + | 'golden_hour' + | 'common_enemy' + | 'double_risk' + | 'jackpot' + | 'swap' + | 'rematch' + +export interface MarathonEvent { + id: number + type: EventType + start_time: string + end_time: string | null + is_active: boolean + created_by: User | null + data: Record | null + created_at: string +} + +export interface EventEffects { + points_multiplier: number + drop_free: boolean + special_action: string | null + description: string +} + +export interface ActiveEvent { + event: MarathonEvent | null + effects: EventEffects + time_remaining_seconds: number | null +} + +export interface EventCreate { + type: EventType + duration_minutes?: number + challenge_id?: number +} + +export const EVENT_INFO: Record = { + golden_hour: { + name: 'Золотой час', + description: 'Все очки x1.5!', + color: 'yellow', + }, + common_enemy: { + name: 'Общий враг', + description: 'Все получают одинаковый челлендж. Первые 3 — бонус!', + color: 'red', + }, + double_risk: { + name: 'Двойной риск', + description: 'Дропы бесплатны, но очки x0.5', + color: 'purple', + }, + jackpot: { + name: 'Джекпот', + description: 'Следующий спин — сложный челлендж с x3 очками!', + color: 'green', + }, + swap: { + name: 'Обмен', + description: 'Можно поменяться заданием с другим участником', + color: 'blue', + }, + rematch: { + name: 'Реванш', + description: 'Можно переделать проваленный челлендж за 50% очков', + color: 'orange', + }, +} + // Admin types export interface AdminUser { id: number