CLAUDE: Implement game creation and lineup submission workflow
Complete implementation of pre-game setup flow allowing players to create games and submit lineups before gameplay starts. Backend Changes: - Extended games.py with create game, lineup submission, and game start endpoints - Added teams.py roster endpoint with season filtering - Enhanced SBA API client with player data fetching and caching - Comprehensive validation for lineup submission (position conflicts, DH rules) Frontend Changes: - Redesigned create.vue with improved team selection and game options - Enhanced index.vue with active/pending game filtering and navigation - Added lineup/[id].vue for interactive lineup builder with drag-and-drop - Implemented auth.client.ts plugin for client-side auth initialization - Added comprehensive TypeScript types for API contracts - Updated middleware for better auth handling Key Features: - Game creation with home/away team selection - Full lineup builder with position assignment and batting order - DH rule validation (pitcher can be excluded from batting order) - Season-based roster filtering (Season 3) - Auto-start game when both lineups submitted - Real-time game list updates Workflow: 1. Create game → select teams → set options 2. Submit home lineup → validate positions/order 3. Submit away lineup → validate positions/order 4. Game auto-starts → navigates to game page 5. WebSocket connection → loads game state Ready for Phase F4 - connecting gameplay UI to complete the at-bat loop. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
58b5deb88e
commit
a87d149788
@ -1,7 +1,12 @@
|
||||
import logging
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from app.core.state_manager import state_manager
|
||||
from app.database.operations import DatabaseOperations
|
||||
from app.services.lineup_service import lineup_service
|
||||
|
||||
logger = logging.getLogger(f"{__name__}.games")
|
||||
|
||||
@ -18,34 +23,384 @@ class GameListItem(BaseModel):
|
||||
away_team_id: int
|
||||
|
||||
|
||||
class CreateGameRequest(BaseModel):
|
||||
"""Request model for creating a new game"""
|
||||
|
||||
name: str = Field(..., description="Game name")
|
||||
home_team_id: int = Field(..., description="Home team ID")
|
||||
away_team_id: int = Field(..., description="Away team ID")
|
||||
is_ai_opponent: bool = Field(default=False, description="Is AI opponent")
|
||||
season: int = Field(default=3, description="Season number")
|
||||
league_id: str = Field(default="sba", description="League ID (sba or pd)")
|
||||
|
||||
|
||||
class CreateGameResponse(BaseModel):
|
||||
"""Response model for game creation"""
|
||||
|
||||
game_id: str
|
||||
message: str
|
||||
status: str
|
||||
|
||||
|
||||
class LineupPlayerRequest(BaseModel):
|
||||
"""Single player in lineup request"""
|
||||
|
||||
player_id: int = Field(..., description="SBA player ID")
|
||||
position: str = Field(..., description="Defensive position (P, C, 1B, etc.)")
|
||||
batting_order: int | None = Field(
|
||||
None, ge=1, le=9, description="Batting order (1-9), null for pitcher in DH lineup"
|
||||
)
|
||||
|
||||
@field_validator("position")
|
||||
@classmethod
|
||||
def validate_position(cls, v: str) -> str:
|
||||
"""Ensure position is valid"""
|
||||
valid = ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF", "DH"]
|
||||
if v not in valid:
|
||||
raise ValueError(f"Position must be one of {valid}")
|
||||
return v
|
||||
|
||||
|
||||
class SubmitLineupsRequest(BaseModel):
|
||||
"""
|
||||
Request model for submitting lineups for both teams.
|
||||
|
||||
Supports both 9-player (pitcher bats) and 10-player (DH) configurations:
|
||||
- 9 players: All have batting orders 1-9
|
||||
- 10 players: Pitcher has batting_order=null, others have 1-9
|
||||
"""
|
||||
|
||||
home_lineup: list[LineupPlayerRequest] = Field(
|
||||
..., min_length=9, max_length=10, description="Home team starting lineup (9-10 players)"
|
||||
)
|
||||
away_lineup: list[LineupPlayerRequest] = Field(
|
||||
..., min_length=9, max_length=10, description="Away team starting lineup (9-10 players)"
|
||||
)
|
||||
|
||||
@field_validator("home_lineup", "away_lineup")
|
||||
@classmethod
|
||||
def validate_lineup(cls, v: list[LineupPlayerRequest]) -> list[LineupPlayerRequest]:
|
||||
"""
|
||||
Validate lineup structure for 9-player or 10-player (DH) configurations.
|
||||
|
||||
Rules:
|
||||
- 9 players: All must have batting orders 1-9
|
||||
- 10 players: Exactly 9 have batting orders 1-9, pitcher has null
|
||||
"""
|
||||
lineup_size = len(v)
|
||||
|
||||
# Check batting orders
|
||||
batters = [p for p in v if p.batting_order is not None]
|
||||
batting_orders = [p.batting_order for p in batters]
|
||||
|
||||
if len(batting_orders) != 9:
|
||||
raise ValueError(f"Must have exactly 9 batters with batting orders, got {len(batting_orders)}")
|
||||
|
||||
if set(batting_orders) != {1, 2, 3, 4, 5, 6, 7, 8, 9}:
|
||||
raise ValueError("Batting orders must be exactly 1-9 with no duplicates")
|
||||
|
||||
# Check positions
|
||||
positions = [p.position for p in v]
|
||||
required_positions = ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF"]
|
||||
|
||||
for req_pos in required_positions:
|
||||
if req_pos not in positions:
|
||||
raise ValueError(f"Missing required position: {req_pos}")
|
||||
|
||||
# For 10-player lineup, must have DH and pitcher must not bat
|
||||
if lineup_size == 10:
|
||||
if "DH" not in positions:
|
||||
raise ValueError("10-player lineup must include DH position")
|
||||
|
||||
pitchers = [p for p in v if p.position == "P"]
|
||||
if len(pitchers) != 1:
|
||||
raise ValueError("Must have exactly 1 pitcher")
|
||||
|
||||
if pitchers[0].batting_order is not None:
|
||||
raise ValueError("Pitcher cannot have batting order in DH lineup")
|
||||
|
||||
# For 9-player lineup, pitcher must bat (no DH)
|
||||
elif lineup_size == 9:
|
||||
if "DH" in positions:
|
||||
raise ValueError("9-player lineup cannot include DH")
|
||||
|
||||
pitchers = [p for p in v if p.position == "P"]
|
||||
if len(pitchers) != 1:
|
||||
raise ValueError("Must have exactly 1 pitcher")
|
||||
|
||||
if pitchers[0].batting_order is None:
|
||||
raise ValueError("Pitcher must have batting order when no DH")
|
||||
|
||||
# Check player uniqueness
|
||||
player_ids = [p.player_id for p in v]
|
||||
if len(set(player_ids)) != len(player_ids):
|
||||
raise ValueError("Players cannot be duplicated in lineup")
|
||||
|
||||
return v
|
||||
|
||||
|
||||
class SubmitLineupsResponse(BaseModel):
|
||||
"""Response model for lineup submission"""
|
||||
|
||||
game_id: str
|
||||
message: str
|
||||
home_lineup_count: int
|
||||
away_lineup_count: int
|
||||
|
||||
|
||||
@router.get("/", response_model=list[GameListItem])
|
||||
async def list_games():
|
||||
"""
|
||||
List all games
|
||||
List all games from the database
|
||||
|
||||
TODO Phase 2: Implement game listing with database query
|
||||
Returns basic game information for all games in the system.
|
||||
TODO: Add user filtering, pagination, and more sophisticated queries
|
||||
"""
|
||||
logger.info("List games endpoint called (stub)")
|
||||
return []
|
||||
try:
|
||||
logger.info("Fetching games list from database")
|
||||
|
||||
db_ops = DatabaseOperations()
|
||||
|
||||
# Get all games from database (for now - later we can add filters)
|
||||
from app.database.session import AsyncSessionLocal
|
||||
from app.models.db_models import Game
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
from sqlalchemy import select
|
||||
result = await session.execute(
|
||||
select(Game).order_by(Game.created_at.desc())
|
||||
)
|
||||
games = result.scalars().all()
|
||||
|
||||
# Convert to response model
|
||||
game_list = [
|
||||
GameListItem(
|
||||
game_id=str(game.id),
|
||||
league_id=game.league_id,
|
||||
status=game.status,
|
||||
home_team_id=game.home_team_id,
|
||||
away_team_id=game.away_team_id,
|
||||
)
|
||||
for game in games
|
||||
]
|
||||
|
||||
logger.info(f"Retrieved {len(game_list)} games from database")
|
||||
return game_list
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to list games: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to list games: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/{game_id}")
|
||||
async def get_game(game_id: str):
|
||||
"""
|
||||
Get game details
|
||||
Get game details including team IDs and status.
|
||||
|
||||
TODO Phase 2: Implement game retrieval
|
||||
Args:
|
||||
game_id: Game identifier
|
||||
|
||||
Returns:
|
||||
Game information including home/away team IDs
|
||||
|
||||
Raises:
|
||||
400: Invalid game_id format
|
||||
404: Game not found
|
||||
"""
|
||||
logger.info(f"Get game {game_id} endpoint called (stub)")
|
||||
return {"game_id": game_id, "message": "Game retrieval stub"}
|
||||
try:
|
||||
# Validate game_id format
|
||||
try:
|
||||
game_uuid = UUID(game_id)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid game_id format")
|
||||
|
||||
logger.info(f"Fetching game details for {game_id}")
|
||||
|
||||
# Get game from state manager
|
||||
if game_uuid not in state_manager._states:
|
||||
raise HTTPException(status_code=404, detail=f"Game {game_id} not found")
|
||||
|
||||
state = state_manager._states[game_uuid]
|
||||
|
||||
return {
|
||||
"game_id": game_id,
|
||||
"status": state.status,
|
||||
"home_team_id": state.home_team_id,
|
||||
"away_team_id": state.away_team_id,
|
||||
"league_id": state.league_id,
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to fetch game {game_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to fetch game: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_game():
|
||||
@router.post("/", response_model=CreateGameResponse)
|
||||
async def create_game(request: CreateGameRequest):
|
||||
"""
|
||||
Create new game
|
||||
|
||||
TODO Phase 2: Implement game creation
|
||||
Creates a new game in the state manager and database.
|
||||
Note: Lineups must be added separately before the game can be started.
|
||||
"""
|
||||
logger.info("Create game endpoint called (stub)")
|
||||
return {"message": "Game creation stub"}
|
||||
try:
|
||||
# Generate game ID
|
||||
game_id = uuid4()
|
||||
logger.info(
|
||||
f"Creating game {game_id}: {request.home_team_id} vs {request.away_team_id}"
|
||||
)
|
||||
|
||||
# Validate teams are different
|
||||
if request.home_team_id == request.away_team_id:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Home and away teams must be different"
|
||||
)
|
||||
|
||||
# Create game in state manager (in-memory)
|
||||
state = await state_manager.create_game(
|
||||
game_id=game_id,
|
||||
league_id=request.league_id,
|
||||
home_team_id=request.home_team_id,
|
||||
away_team_id=request.away_team_id,
|
||||
)
|
||||
|
||||
# Save to database
|
||||
db_ops = DatabaseOperations()
|
||||
await db_ops.create_game(
|
||||
game_id=game_id,
|
||||
league_id=request.league_id,
|
||||
home_team_id=request.home_team_id,
|
||||
away_team_id=request.away_team_id,
|
||||
game_mode="friendly" if not request.is_ai_opponent else "ai",
|
||||
visibility="public",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Game {game_id} created successfully - status: {state.status}"
|
||||
)
|
||||
|
||||
return CreateGameResponse(
|
||||
game_id=str(game_id),
|
||||
message=f"Game '{request.name}' created successfully. Add lineups to start the game.",
|
||||
status=state.status,
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to create game: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create game: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/{game_id}/lineups", response_model=SubmitLineupsResponse)
|
||||
async def submit_lineups(game_id: str, request: SubmitLineupsRequest):
|
||||
"""
|
||||
Submit lineups for both teams.
|
||||
|
||||
Accepts complete lineups for home and away teams. Supports both:
|
||||
- 9-player lineups (pitcher bats, no DH)
|
||||
- 10-player lineups (universal DH, pitcher doesn't bat)
|
||||
|
||||
Args:
|
||||
game_id: Game identifier
|
||||
request: Lineup data for both teams
|
||||
|
||||
Returns:
|
||||
Confirmation with lineup counts
|
||||
|
||||
Raises:
|
||||
400: Invalid lineup structure or validation error
|
||||
404: Game not found
|
||||
500: Database or API error
|
||||
"""
|
||||
try:
|
||||
# Validate game_id format
|
||||
try:
|
||||
game_uuid = UUID(game_id)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid game_id format")
|
||||
|
||||
logger.info(f"Submitting lineups for game {game_id}")
|
||||
|
||||
# Get game state from memory or load basic info from database
|
||||
state = state_manager.get_state(game_uuid)
|
||||
if not state:
|
||||
logger.info(f"Game {game_id} not in memory, loading from database")
|
||||
|
||||
# Load basic game info from database
|
||||
db_ops = DatabaseOperations()
|
||||
game_data = await db_ops.load_game_state(game_uuid)
|
||||
|
||||
if not game_data:
|
||||
raise HTTPException(status_code=404, detail=f"Game {game_id} not found")
|
||||
|
||||
game_info = game_data['game']
|
||||
|
||||
# Recreate game state in memory (without lineups - we're about to add them)
|
||||
state = await state_manager.create_game(
|
||||
game_id=game_uuid,
|
||||
league_id=game_info['league_id'],
|
||||
home_team_id=game_info['home_team_id'],
|
||||
away_team_id=game_info['away_team_id'],
|
||||
home_team_is_ai=game_info.get('home_team_is_ai', False),
|
||||
away_team_is_ai=game_info.get('away_team_is_ai', False),
|
||||
auto_mode=game_info.get('auto_mode', False)
|
||||
)
|
||||
logger.info(f"Recreated game {game_id} in memory from database")
|
||||
|
||||
# Process home team lineup
|
||||
home_count = 0
|
||||
for player in request.home_lineup:
|
||||
await lineup_service.add_sba_player_to_lineup(
|
||||
game_id=game_uuid,
|
||||
team_id=state.home_team_id,
|
||||
player_id=player.player_id,
|
||||
position=player.position,
|
||||
batting_order=player.batting_order,
|
||||
is_starter=True,
|
||||
)
|
||||
home_count += 1
|
||||
|
||||
logger.info(f"Added {home_count} players to home team lineup")
|
||||
|
||||
# Process away team lineup
|
||||
away_count = 0
|
||||
for player in request.away_lineup:
|
||||
await lineup_service.add_sba_player_to_lineup(
|
||||
game_id=game_uuid,
|
||||
team_id=state.away_team_id,
|
||||
player_id=player.player_id,
|
||||
position=player.position,
|
||||
batting_order=player.batting_order,
|
||||
is_starter=True,
|
||||
)
|
||||
away_count += 1
|
||||
|
||||
logger.info(f"Added {away_count} players to away team lineup")
|
||||
|
||||
# Automatically start the game after lineups are submitted
|
||||
from app.core.game_engine import game_engine
|
||||
try:
|
||||
await game_engine.start_game(game_uuid)
|
||||
logger.info(f"Game {game_id} started successfully after lineup submission")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to auto-start game {game_id}: {e}")
|
||||
# Don't fail the lineup submission if game start fails
|
||||
# User can manually start if needed
|
||||
|
||||
return SubmitLineupsResponse(
|
||||
game_id=game_id,
|
||||
message=f"Lineups submitted successfully. Home: {home_count} players, Away: {away_count} players.",
|
||||
home_lineup_count=home_count,
|
||||
away_lineup_count=away_count,
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to submit lineups for game {game_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to submit lineups: {str(e)}"
|
||||
)
|
||||
|
||||
@ -57,3 +57,31 @@ async def get_teams(season: int = Query(..., description="Season number (e.g., 3
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch teams: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Failed to fetch teams")
|
||||
|
||||
|
||||
@router.get("/{team_id}/roster")
|
||||
async def get_team_roster(
|
||||
team_id: int,
|
||||
season: int = Query(..., description="Season number (e.g., 3)"),
|
||||
):
|
||||
"""
|
||||
Get roster for a specific team from SBA API.
|
||||
|
||||
Args:
|
||||
team_id: Team ID
|
||||
season: Season number to fetch roster for
|
||||
|
||||
Returns:
|
||||
List of players on the team's roster
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Fetching roster for team {team_id}, season {season}")
|
||||
roster = await sba_api_client.get_roster(team_id=team_id, season=season)
|
||||
|
||||
return {"count": len(roster), "players": roster}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch roster for team {team_id}: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to fetch roster for team {team_id}"
|
||||
)
|
||||
|
||||
@ -130,6 +130,46 @@ class SbaApiClient:
|
||||
logger.error(f"Unexpected error fetching player {player_id}: {e}")
|
||||
raise
|
||||
|
||||
async def get_roster(self, team_id: int, season: int) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Fetch roster for a specific team from SBA API.
|
||||
|
||||
Args:
|
||||
team_id: Team ID
|
||||
season: Season number (e.g., 3 for Season 3)
|
||||
|
||||
Returns:
|
||||
List of player dictionaries from the SBA API
|
||||
|
||||
Example:
|
||||
roster = await client.get_roster(team_id=35, season=3)
|
||||
for player in roster:
|
||||
print(f"{player['name']}: {player['position']}")
|
||||
"""
|
||||
url = f"{self.base_url}/players"
|
||||
params = {"season": season, "team_id": team_id}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.get(
|
||||
url, headers=self._get_headers(), params=params
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
players = data.get("players", [])
|
||||
count = data.get("count", len(players))
|
||||
|
||||
logger.info(f"Loaded {count} players for team {team_id}, season {season}")
|
||||
return players
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"Failed to fetch roster for team {team_id}: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error fetching roster: {e}")
|
||||
raise
|
||||
|
||||
async def get_players_batch(self, player_ids: list[int]) -> dict[int, SbaPlayer]:
|
||||
"""
|
||||
Fetch multiple players in parallel.
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
"nuxt": "^4.1.3",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"vue": "^3.5.22",
|
||||
"vue-draggable-plus": "^0.6.0",
|
||||
"vue-router": "^4.6.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -452,6 +453,8 @@
|
||||
|
||||
"@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="],
|
||||
|
||||
"@types/sortablejs": ["@types/sortablejs@1.15.9", "", {}, "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@6.21.0", "", { "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" }, "peerDependencies": { "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@6.21.0", "", { "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" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ=="],
|
||||
@ -1962,6 +1965,8 @@
|
||||
|
||||
"vue-devtools-stub": ["vue-devtools-stub@0.1.0", "", {}, "sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ=="],
|
||||
|
||||
"vue-draggable-plus": ["vue-draggable-plus@0.6.0", "", { "dependencies": { "@types/sortablejs": "^1.15.8" } }, "sha512-G5TSfHrt9tX9EjdG49InoFJbt2NYk0h3kgjgKxkFWr3ulIUays0oFObr5KZ8qzD4+QnhtALiRwIqY6qul4egqw=="],
|
||||
|
||||
"vue-eslint-parser": ["vue-eslint-parser@9.4.3", "", { "dependencies": { "debug": "^4.3.4", "eslint-scope": "^7.1.1", "eslint-visitor-keys": "^3.3.0", "espree": "^9.3.1", "esquery": "^1.4.0", "lodash": "^4.17.21", "semver": "^7.3.6" }, "peerDependencies": { "eslint": ">=6.0.0" } }, "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg=="],
|
||||
|
||||
"vue-router": ["vue-router@4.6.3", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg=="],
|
||||
@ -2044,10 +2049,6 @@
|
||||
|
||||
"@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"@eslint/eslintrc/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"@humanwhocodes/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
|
||||
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
@ -2060,8 +2061,6 @@
|
||||
|
||||
"@nuxt/cli/tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="],
|
||||
|
||||
"@nuxt/devtools/@nuxt/kit": ["@nuxt/kit@3.19.3", "", { "dependencies": { "c12": "^3.3.0", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.7", "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", "knitwork": "^1.2.0", "mlly": "^1.8.0", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "rc9": "^2.1.2", "scule": "^1.3.0", "semver": "^7.7.2", "std-env": "^3.9.0", "tinyglobby": "^0.2.15", "ufo": "^1.6.1", "unctx": "^2.4.1", "unimport": "^5.4.1", "untyped": "^2.0.0" } }, "sha512-ze46EW5xW+UxDvinvPkYt2MzR355Az1lA3bpX8KDialgnCwr+IbkBij/udbUEC6ZFbidPkfK1eKl4ESN7gMY+w=="],
|
||||
|
||||
"@nuxt/devtools/local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="],
|
||||
|
||||
"@nuxt/devtools/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
@ -2070,8 +2069,6 @@
|
||||
|
||||
"@nuxt/devtools/vite": ["vite@7.1.11", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "tsx"], "bin": "bin/vite.js" }, "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg=="],
|
||||
|
||||
"@nuxt/devtools-kit/@nuxt/kit": ["@nuxt/kit@3.19.3", "", { "dependencies": { "c12": "^3.3.0", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.7", "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", "knitwork": "^1.2.0", "mlly": "^1.8.0", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "rc9": "^2.1.2", "scule": "^1.3.0", "semver": "^7.7.2", "std-env": "^3.9.0", "tinyglobby": "^0.2.15", "ufo": "^1.6.1", "unctx": "^2.4.1", "unimport": "^5.4.1", "untyped": "^2.0.0" } }, "sha512-ze46EW5xW+UxDvinvPkYt2MzR355Az1lA3bpX8KDialgnCwr+IbkBij/udbUEC6ZFbidPkfK1eKl4ESN7gMY+w=="],
|
||||
|
||||
"@nuxt/devtools-kit/vite": ["vite@7.1.11", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "tsx"], "bin": "bin/vite.js" }, "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg=="],
|
||||
|
||||
"@nuxt/devtools-wizard/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
@ -2080,8 +2077,6 @@
|
||||
|
||||
"@nuxt/schema/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"@nuxt/telemetry/@nuxt/kit": ["@nuxt/kit@3.19.3", "", { "dependencies": { "c12": "^3.3.0", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.7", "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", "knitwork": "^1.2.0", "mlly": "^1.8.0", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "rc9": "^2.1.2", "scule": "^1.3.0", "semver": "^7.7.2", "std-env": "^3.9.0", "tinyglobby": "^0.2.15", "ufo": "^1.6.1", "unctx": "^2.4.1", "unimport": "^5.4.1", "untyped": "^2.0.0" } }, "sha512-ze46EW5xW+UxDvinvPkYt2MzR355Az1lA3bpX8KDialgnCwr+IbkBij/udbUEC6ZFbidPkfK1eKl4ESN7gMY+w=="],
|
||||
|
||||
"@nuxt/telemetry/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
||||
|
||||
"@nuxt/telemetry/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
@ -2096,8 +2091,6 @@
|
||||
|
||||
"@nuxtjs/tailwindcss/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"@pinia/nuxt/@nuxt/kit": ["@nuxt/kit@3.19.3", "", { "dependencies": { "c12": "^3.3.0", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.7", "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", "knitwork": "^1.2.0", "mlly": "^1.8.0", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "rc9": "^2.1.2", "scule": "^1.3.0", "semver": "^7.7.2", "std-env": "^3.9.0", "tinyglobby": "^0.2.15", "ufo": "^1.6.1", "unctx": "^2.4.1", "unimport": "^5.4.1", "untyped": "^2.0.0" } }, "sha512-ze46EW5xW+UxDvinvPkYt2MzR355Az1lA3bpX8KDialgnCwr+IbkBij/udbUEC6ZFbidPkfK1eKl4ESN7gMY+w=="],
|
||||
|
||||
"@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
|
||||
|
||||
"@rollup/plugin-commonjs/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
@ -2118,10 +2111,6 @@
|
||||
|
||||
"@vitejs/plugin-vue-jsx/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.44", "", {}, "sha512-g6eW7Zwnr2c5RADIoqziHoVs6b3W5QTQ4+qbpfjbkMJ9x+8Og211VW/oot2dj9dVwaK/UyC6Yo+02gV+wWQVNg=="],
|
||||
|
||||
"@vitest/runner/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||
|
||||
"@vitest/snapshot/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||
|
||||
"@vue-macros/common/local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="],
|
||||
|
||||
"@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
@ -2134,8 +2123,6 @@
|
||||
|
||||
"@vue/devtools-kit/perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
|
||||
|
||||
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"archiver-utils/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
@ -2144,20 +2131,12 @@
|
||||
|
||||
"c12/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"cache-content-type/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"clean-regexp/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
|
||||
|
||||
"clipboardy/execa": ["execa@9.6.0", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw=="],
|
||||
|
||||
"clipboardy/is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
|
||||
|
||||
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
@ -2184,8 +2163,6 @@
|
||||
|
||||
"eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"eslint/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||
|
||||
"eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||
@ -2198,16 +2175,12 @@
|
||||
|
||||
"eslint-plugin-n/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"eslint-plugin-n/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"eslint-plugin-node/eslint-plugin-es": ["eslint-plugin-es@3.0.1", "", { "dependencies": { "eslint-utils": "^2.0.0", "regexpp": "^3.0.0" }, "peerDependencies": { "eslint": ">=4.19.1" } }, "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ=="],
|
||||
|
||||
"eslint-plugin-node/eslint-utils": ["eslint-utils@2.1.0", "", { "dependencies": { "eslint-visitor-keys": "^1.1.0" } }, "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg=="],
|
||||
|
||||
"eslint-plugin-node/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"eslint-plugin-node/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"eslint-plugin-node/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@2.1.0", "", {}, "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw=="],
|
||||
@ -2244,8 +2217,6 @@
|
||||
|
||||
"listhen/clipboardy": ["clipboardy@4.0.0", "", { "dependencies": { "execa": "^8.0.1", "is-wsl": "^3.1.0", "is64bit": "^2.0.0" } }, "sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w=="],
|
||||
|
||||
"listhen/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
@ -2270,8 +2241,6 @@
|
||||
|
||||
"open/is-docker": ["is-docker@2.2.1", "", { "bin": "cli.js" }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
|
||||
|
||||
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"pinia/@vue/devtools-api": ["@vue/devtools-api@7.7.7", "", { "dependencies": { "@vue/devtools-kit": "^7.7.7" } }, "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg=="],
|
||||
|
||||
"pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
@ -2284,8 +2253,6 @@
|
||||
|
||||
"postcss-minify-selectors/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
|
||||
|
||||
"postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
|
||||
|
||||
"postcss-nesting/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
|
||||
|
||||
"postcss-unique-selectors/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
|
||||
@ -2308,8 +2275,6 @@
|
||||
|
||||
"rollup-plugin-visualizer/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
|
||||
|
||||
"safe-push-apply/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"send/encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"send/fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||
@ -2326,14 +2291,6 @@
|
||||
|
||||
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"stylehacks/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
|
||||
|
||||
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
||||
@ -2342,20 +2299,14 @@
|
||||
|
||||
"tailwindcss/chokidar": ["chokidar@3.6.0", "", { "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" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||
|
||||
"tailwindcss/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"tailwindcss/jiti": ["jiti@1.21.7", "", { "bin": "bin/jiti.js" }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
|
||||
|
||||
"tailwindcss/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
|
||||
|
||||
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||
|
||||
"test-exclude/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": "lib/cli.js" }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
|
||||
|
||||
"type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"unenv/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"unimport/local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="],
|
||||
@ -2370,14 +2321,10 @@
|
||||
|
||||
"unplugin-vue-router/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"untun/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||
|
||||
"unwasm/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"vite/esbuild": ["esbuild@0.21.5", "", { "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" }, "bin": "bin/esbuild" }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
|
||||
|
||||
"vite-node/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||
|
||||
"vite-plugin-checker/npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
|
||||
|
||||
"vite-plugin-inspect/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
||||
@ -2390,52 +2337,30 @@
|
||||
|
||||
"vite-plugin-vue-tracer/vite": ["vite@7.1.11", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "tsx"], "bin": "bin/vite.js" }, "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg=="],
|
||||
|
||||
"vitest/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||
|
||||
"whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"wrap-ansi/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
|
||||
"wrap-ansi/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
|
||||
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"wsl-utils/is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
|
||||
|
||||
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"@humanwhocodes/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"@mapbox/node-pre-gyp/nopt/abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="],
|
||||
|
||||
"@nuxt/devtools-kit/@nuxt/kit/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"@pinia/nuxt/@nuxt/kit/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/globby/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"cache-content-type/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"clipboardy/execa/get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
|
||||
|
||||
"clipboardy/execa/human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
|
||||
@ -2446,12 +2371,6 @@
|
||||
|
||||
"clipboardy/execa/strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
|
||||
|
||||
"cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
|
||||
@ -2460,24 +2379,14 @@
|
||||
|
||||
"eslint-plugin-es/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@1.3.0", "", {}, "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ=="],
|
||||
|
||||
"eslint-plugin-n/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"eslint-plugin-node/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@1.3.0", "", {}, "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ=="],
|
||||
|
||||
"eslint-plugin-node/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"eslint/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"http-assert/http-errors/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="],
|
||||
|
||||
"http-assert/http-errors/statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="],
|
||||
|
||||
"koa-send/http-errors/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="],
|
||||
|
||||
"koa-send/http-errors/statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="],
|
||||
|
||||
"koa/http-errors/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="],
|
||||
|
||||
"lazystream/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
|
||||
@ -2494,38 +2403,24 @@
|
||||
|
||||
"readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"replace-in-file/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"resolve-path/http-errors/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="],
|
||||
|
||||
"resolve-path/http-errors/inherits": ["inherits@2.0.3", "", {}, "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="],
|
||||
|
||||
"resolve-path/http-errors/setprototypeof": ["setprototypeof@1.1.0", "", {}, "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="],
|
||||
|
||||
"resolve-path/http-errors/statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="],
|
||||
|
||||
"rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"rollup-plugin-visualizer/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
|
||||
|
||||
"rollup-plugin-visualizer/open/is-docker": ["is-docker@2.2.1", "", { "bin": "cli.js" }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
|
||||
|
||||
"rollup-plugin-visualizer/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
|
||||
|
||||
"send/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
|
||||
"test-exclude/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"vite-plugin-checker/npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
|
||||
"vite-plugin-inspect/unplugin-utils/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
@ -2576,10 +2471,6 @@
|
||||
|
||||
"vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
@ -2588,10 +2479,6 @@
|
||||
|
||||
"read-pkg-up/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
||||
|
||||
"replace-in-file/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"read-pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
*
|
||||
* Protects routes that require authentication.
|
||||
* Redirects to login if user is not authenticated.
|
||||
*
|
||||
* Note: Auth state is initialized by the auth.client.ts plugin before this middleware runs.
|
||||
*/
|
||||
|
||||
import { useAuthStore } from '~/store/auth'
|
||||
@ -10,17 +12,27 @@ import { useAuthStore } from '~/store/auth'
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// Initialize auth from localStorage if not already done
|
||||
if (process.client && !authStore.isAuthenticated) {
|
||||
authStore.initializeAuth()
|
||||
}
|
||||
console.log('[Auth Middleware]', {
|
||||
path: to.path,
|
||||
isAuthenticated: authStore.isAuthenticated,
|
||||
isTokenValid: authStore.isTokenValid,
|
||||
hasUser: !!authStore.currentUser,
|
||||
})
|
||||
|
||||
// Allow access if authenticated
|
||||
if (authStore.isAuthenticated) {
|
||||
// Allow access if authenticated and token is valid
|
||||
if (authStore.isAuthenticated && authStore.isTokenValid) {
|
||||
return
|
||||
}
|
||||
|
||||
// If token expired but we have a refresh token, try refreshing
|
||||
if (authStore.isAuthenticated && !authStore.isTokenValid && process.client) {
|
||||
console.log('[Auth Middleware] Token expired, attempting refresh')
|
||||
// Don't await - let it refresh in background and redirect for now
|
||||
authStore.refreshAccessToken()
|
||||
}
|
||||
|
||||
// Redirect to login with return URL
|
||||
console.log('[Auth Middleware] Redirecting to login')
|
||||
return navigateTo({
|
||||
path: '/auth/login',
|
||||
query: { redirect: to.fullPath },
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
"nuxt": "^4.1.3",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"vue": "^3.5.22",
|
||||
"vue-draggable-plus": "^0.6.0",
|
||||
"vue-router": "^4.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -32,16 +32,19 @@
|
||||
<select
|
||||
id="homeTeam"
|
||||
v-model="formData.homeTeamId"
|
||||
:disabled="isLoadingTeams"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||
>
|
||||
<option value="" disabled>Select home team</option>
|
||||
<option value="" disabled>
|
||||
{{ isLoadingTeams ? 'Loading teams...' : 'Select home team' }}
|
||||
</option>
|
||||
<option
|
||||
v-for="team in authStore.userTeams"
|
||||
v-for="team in availableTeams"
|
||||
:key="team.id"
|
||||
:value="team.id"
|
||||
>
|
||||
{{ team.name }} ({{ team.abbreviation }})
|
||||
{{ team.lname }} ({{ team.abbrev }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@ -54,56 +57,24 @@
|
||||
<select
|
||||
id="awayTeam"
|
||||
v-model="formData.awayTeamId"
|
||||
:disabled="isLoadingTeams"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||
>
|
||||
<option value="" disabled>Select away team</option>
|
||||
<option value="" disabled>
|
||||
{{ isLoadingTeams ? 'Loading teams...' : 'Select away team' }}
|
||||
</option>
|
||||
<option
|
||||
v-for="team in authStore.userTeams"
|
||||
v-for="team in availableTeams"
|
||||
:key="team.id"
|
||||
:value="team.id"
|
||||
:disabled="team.id === formData.homeTeamId"
|
||||
>
|
||||
{{ team.name }} ({{ team.abbreviation }})
|
||||
{{ team.lname }} ({{ team.abbrev }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Game Mode -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Game Mode
|
||||
</label>
|
||||
<div class="space-y-3">
|
||||
<label class="flex items-center p-4 border border-gray-300 rounded-lg cursor-pointer hover:bg-gray-50 transition">
|
||||
<input
|
||||
v-model="formData.isAiOpponent"
|
||||
type="radio"
|
||||
:value="false"
|
||||
name="gameMode"
|
||||
class="mr-3 text-primary focus:ring-primary"
|
||||
/>
|
||||
<div>
|
||||
<div class="font-medium text-gray-900">Human vs Human</div>
|
||||
<div class="text-sm text-gray-600">Play against another player</div>
|
||||
</div>
|
||||
</label>
|
||||
<label class="flex items-center p-4 border border-gray-300 rounded-lg cursor-pointer hover:bg-gray-50 transition">
|
||||
<input
|
||||
v-model="formData.isAiOpponent"
|
||||
type="radio"
|
||||
:value="true"
|
||||
name="gameMode"
|
||||
class="mr-3 text-primary focus:ring-primary"
|
||||
/>
|
||||
<div>
|
||||
<div class="font-medium text-gray-900">Human vs AI</div>
|
||||
<div class="text-sm text-gray-600">Play against the computer</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div
|
||||
v-if="error"
|
||||
@ -122,27 +93,19 @@
|
||||
</NuxtLink>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="isLoading"
|
||||
:disabled="isLoading || isLoadingTeams"
|
||||
class="px-6 py-3 bg-primary hover:bg-blue-700 disabled:bg-gray-400 text-white font-semibold rounded-lg transition disabled:cursor-not-allowed"
|
||||
>
|
||||
{{ isLoading ? 'Creating...' : 'Create Game' }}
|
||||
{{ isLoadingTeams ? 'Loading...' : isLoading ? 'Creating...' : 'Create Game' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Info Box -->
|
||||
<div class="mt-6 bg-blue-50 border border-blue-200 rounded-lg p-6">
|
||||
<h3 class="font-bold text-blue-900 mb-2">Coming in Phase F6</h3>
|
||||
<p class="text-blue-800 text-sm">
|
||||
This is a placeholder form. Full game creation with team selection, lineup management,
|
||||
and game settings will be implemented in Phase F6 (Game Management).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from '~/store/auth'
|
||||
import type { SbaTeam } from '~/types/api'
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['auth'], // Require authentication
|
||||
@ -150,15 +113,33 @@ definePageMeta({
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const isLoading = ref(false)
|
||||
const isLoadingTeams = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
const availableTeams = ref<SbaTeam[]>([])
|
||||
|
||||
const formData = ref({
|
||||
name: '',
|
||||
homeTeamId: '',
|
||||
awayTeamId: '',
|
||||
isAiOpponent: false,
|
||||
})
|
||||
|
||||
// Fetch teams on component mount
|
||||
onMounted(async () => {
|
||||
try {
|
||||
isLoadingTeams.value = true
|
||||
const teams = await $fetch<SbaTeam[]>(`${config.public.apiUrl}/api/teams/`, {
|
||||
params: { season: 3 },
|
||||
})
|
||||
availableTeams.value = teams
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load teams:', err)
|
||||
error.value = 'Failed to load teams. Please refresh the page.'
|
||||
} finally {
|
||||
isLoadingTeams.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const handleCreateGame = async () => {
|
||||
@ -177,20 +158,26 @@ const handleCreateGame = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO Phase F6: Call API to create game
|
||||
// const response = await $fetch('/api/games', {
|
||||
// method: 'POST',
|
||||
// body: formData.value
|
||||
// })
|
||||
// Call API to create game
|
||||
const response = await $fetch<{ game_id: string; message: string; status: string }>(
|
||||
`${config.public.apiUrl}/api/games/`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: {
|
||||
name: formData.value.name,
|
||||
home_team_id: formData.value.homeTeamId,
|
||||
away_team_id: formData.value.awayTeamId,
|
||||
is_ai_opponent: false, // SBA only supports human vs human
|
||||
season: 3,
|
||||
league_id: 'sba',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// For now, just show a placeholder success message
|
||||
alert('Game creation will be implemented in Phase F6')
|
||||
|
||||
// Redirect to games list
|
||||
// router.push(`/games/${response.game_id}`)
|
||||
router.push('/games')
|
||||
// Redirect to lineup builder
|
||||
router.push(`/games/lineup/${response.game_id}`)
|
||||
} catch (err: any) {
|
||||
error.value = err.message || 'Failed to create game'
|
||||
error.value = err.data?.detail || err.message || 'Failed to create game'
|
||||
console.error('Create game error:', err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
|
||||
@ -43,10 +43,57 @@
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="bg-white rounded-lg shadow-md p-12 text-center">
|
||||
<div class="w-16 h-16 mx-auto mb-4 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
|
||||
<p class="text-gray-900 font-semibold">Loading games...</p>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-else-if="error" class="bg-red-50 border border-red-200 rounded-lg p-6">
|
||||
<p class="text-red-800 font-semibold">Failed to load games</p>
|
||||
<p class="text-red-600 text-sm mt-2">{{ error }}</p>
|
||||
<button
|
||||
@click="fetchGames"
|
||||
class="mt-4 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Games List -->
|
||||
<div v-if="activeTab === 'active'">
|
||||
<div v-else-if="activeTab === 'active'">
|
||||
<!-- Games List -->
|
||||
<div v-if="activeGames.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<NuxtLink
|
||||
v-for="game in activeGames"
|
||||
:key="game.game_id"
|
||||
:to="`/games/${game.game_id}`"
|
||||
class="bg-white rounded-lg shadow-md hover:shadow-xl transition p-6"
|
||||
>
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<span :class="[
|
||||
'px-3 py-1 rounded-full text-sm font-semibold',
|
||||
game.status === 'active' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
|
||||
]">
|
||||
{{ game.status === 'active' ? 'In Progress' : 'Pending Lineups' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-600">Away</span>
|
||||
<span class="font-bold">Team {{ game.away_team_id }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-600">Home</span>
|
||||
<span class="font-bold">Team {{ game.home_team_id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div class="bg-white rounded-lg shadow-md p-12 text-center">
|
||||
<div v-else class="bg-white rounded-lg shadow-md p-12 text-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-16 w-16 mx-auto text-gray-400 mb-4"
|
||||
@ -74,16 +121,37 @@
|
||||
Create Your First Game
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- TODO Phase F6: Replace with actual games list -->
|
||||
<!-- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<GameCard v-for="game in activeGames" :key="game.id" :game="game" />
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeTab === 'completed'">
|
||||
<!-- Completed Games List -->
|
||||
<div v-if="completedGames.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<NuxtLink
|
||||
v-for="game in completedGames"
|
||||
:key="game.game_id"
|
||||
:to="`/games/${game.game_id}`"
|
||||
class="bg-white rounded-lg shadow-md hover:shadow-xl transition p-6"
|
||||
>
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<span class="px-3 py-1 rounded-full text-sm font-semibold bg-gray-100 text-gray-800">
|
||||
Completed
|
||||
</span>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-600">Away</span>
|
||||
<span class="font-bold">Team {{ game.away_team_id }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-600">Home</span>
|
||||
<span class="font-bold">Team {{ game.home_team_id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div class="bg-white rounded-lg shadow-md p-12 text-center">
|
||||
<div v-else class="bg-white rounded-lg shadow-md p-12 text-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-16 w-16 mx-auto text-gray-400 mb-4"
|
||||
@ -105,8 +173,6 @@
|
||||
You haven't completed any games yet.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- TODO Phase F6: Replace with actual completed games list -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -116,11 +182,52 @@ definePageMeta({
|
||||
middleware: ['auth'], // Require authentication
|
||||
})
|
||||
|
||||
const activeTab = ref<'active' | 'completed'>('active')
|
||||
import { useAuthStore } from '~/store/auth'
|
||||
|
||||
// TODO Phase F6: Fetch games from API
|
||||
// const { data: activeGames } = await useFetch('/api/games?status=active')
|
||||
// const { data: completedGames } = await useFetch('/api/games?status=completed')
|
||||
const activeTab = ref<'active' | 'completed'>('active')
|
||||
const config = useRuntimeConfig()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// Games data
|
||||
const games = ref<any[]>([])
|
||||
const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
// Fetch games from API (client-side only)
|
||||
async function fetchGames() {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
const response = await $fetch<any[]>(`${config.public.apiUrl}/api/games/`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authStore.token}`
|
||||
}
|
||||
})
|
||||
|
||||
games.value = response
|
||||
console.log('[Games Page] Fetched games:', games.value)
|
||||
} catch (err: any) {
|
||||
console.error('[Games Page] Failed to fetch games:', err)
|
||||
error.value = err.message || 'Failed to load games'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Filter games by status
|
||||
const activeGames = computed(() => {
|
||||
return games.value?.filter(g => g.status === 'active' || g.status === 'pending') || []
|
||||
})
|
||||
|
||||
const completedGames = computed(() => {
|
||||
return games.value?.filter(g => g.status === 'completed' || g.status === 'final') || []
|
||||
})
|
||||
|
||||
// Fetch on mount (client-side)
|
||||
onMounted(() => {
|
||||
fetchGames()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
548
frontend-sba/pages/games/lineup/[id].vue
Normal file
548
frontend-sba/pages/games/lineup/[id].vue
Normal file
@ -0,0 +1,548 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import type { SbaPlayer, LineupPlayerRequest, SubmitLineupsRequest } from '~/types'
|
||||
import ActionButton from '~/components/UI/ActionButton.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
// Game and team data
|
||||
const gameId = ref(route.params.id as string)
|
||||
const homeTeamId = ref<number | null>(null)
|
||||
const awayTeamId = ref<number | null>(null)
|
||||
const season = ref(3)
|
||||
|
||||
// Active tab (home or away)
|
||||
type TeamTab = 'home' | 'away'
|
||||
const activeTab = ref<TeamTab>('away') // Away bats first
|
||||
|
||||
// Roster data
|
||||
const homeRoster = ref<SbaPlayer[]>([])
|
||||
const awayRoster = ref<SbaPlayer[]>([])
|
||||
const loadingRoster = ref(false)
|
||||
const submittingLineups = ref(false)
|
||||
|
||||
// Lineup state - 10 slots each (1-9 batting, 10 pitcher)
|
||||
interface LineupSlot {
|
||||
player: SbaPlayer | null
|
||||
position: string | null
|
||||
battingOrder: number | null // 1-9 for batters, null for pitcher
|
||||
}
|
||||
|
||||
const homeLineup = ref<LineupSlot[]>(Array(10).fill(null).map((_, i) => ({
|
||||
player: null,
|
||||
position: null,
|
||||
battingOrder: i < 9 ? i + 1 : null // Slots 0-8 are batting order 1-9, slot 9 is pitcher
|
||||
})))
|
||||
|
||||
const awayLineup = ref<LineupSlot[]>(Array(10).fill(null).map((_, i) => ({
|
||||
player: null,
|
||||
position: null,
|
||||
battingOrder: i < 9 ? i + 1 : null
|
||||
})))
|
||||
|
||||
// Available roster for dragging (players not in lineup)
|
||||
const availableHomeRoster = computed(() => {
|
||||
const usedPlayerIds = new Set(homeLineup.value.filter(s => s.player).map(s => s.player!.id))
|
||||
return homeRoster.value.filter(p => !usedPlayerIds.has(p.id))
|
||||
})
|
||||
|
||||
const availableAwayRoster = computed(() => {
|
||||
const usedPlayerIds = new Set(awayLineup.value.filter(s => s.player).map(s => s.player!.id))
|
||||
return awayRoster.value.filter(p => !usedPlayerIds.has(p.id))
|
||||
})
|
||||
|
||||
// Current lineup based on active tab
|
||||
const currentLineup = computed(() => activeTab.value === 'home' ? homeLineup.value : awayLineup.value)
|
||||
const currentRoster = computed(() => activeTab.value === 'home' ? availableHomeRoster.value : availableAwayRoster.value)
|
||||
|
||||
// Slot 10 (pitcher) should be disabled if P is selected in batting order
|
||||
const pitcherSlotDisabled = computed(() => {
|
||||
const lineup = currentLineup.value
|
||||
return lineup.slice(0, 9).some(slot => slot.position === 'P')
|
||||
})
|
||||
|
||||
// Validation
|
||||
const duplicatePositions = computed(() => {
|
||||
const lineup = currentLineup.value
|
||||
const positionCounts = new Map<string, number>()
|
||||
|
||||
lineup.forEach(slot => {
|
||||
if (slot.player && slot.position) {
|
||||
positionCounts.set(slot.position, (positionCounts.get(slot.position) || 0) + 1)
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(positionCounts.entries())
|
||||
.filter(([_, count]) => count > 1)
|
||||
.map(([pos, _]) => pos)
|
||||
})
|
||||
|
||||
const validationErrors = computed(() => {
|
||||
const errors: string[] = []
|
||||
|
||||
// Check both lineups
|
||||
const homeErrors = validateLineup(homeLineup.value, 'Home')
|
||||
const awayErrors = validateLineup(awayLineup.value, 'Away')
|
||||
|
||||
return [...homeErrors, ...awayErrors]
|
||||
})
|
||||
|
||||
function validateLineup(lineup: LineupSlot[], teamName: string): string[] {
|
||||
const errors: string[] = []
|
||||
|
||||
// Check if P is in batting order
|
||||
const pitcherInBattingOrder = lineup.slice(0, 9).some(s => s.position === 'P')
|
||||
const requiredSlots = pitcherInBattingOrder ? 9 : 10
|
||||
|
||||
// Check if all slots filled
|
||||
const filledSlots = lineup.filter(s => s.player).length
|
||||
if (filledSlots < requiredSlots) {
|
||||
errors.push(`${teamName}: Fill all ${requiredSlots} lineup slots`)
|
||||
}
|
||||
|
||||
// Check for missing positions
|
||||
const missingPositions = lineup.filter(s => s.player && !s.position).length
|
||||
if (missingPositions > 0) {
|
||||
errors.push(`${teamName}: ${missingPositions} player${missingPositions > 1 ? 's' : ''} missing position`)
|
||||
}
|
||||
|
||||
// Check for duplicate positions
|
||||
const positionCounts = new Map<string, number>()
|
||||
lineup.forEach(slot => {
|
||||
if (slot.player && slot.position) {
|
||||
positionCounts.set(slot.position, (positionCounts.get(slot.position) || 0) + 1)
|
||||
}
|
||||
})
|
||||
|
||||
const duplicates = Array.from(positionCounts.entries())
|
||||
.filter(([_, count]) => count > 1)
|
||||
.map(([pos, _]) => pos)
|
||||
|
||||
if (duplicates.length > 0) {
|
||||
errors.push(`${teamName}: Duplicate positions - ${duplicates.join(', ')}`)
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
return validationErrors.value.length === 0
|
||||
})
|
||||
|
||||
// Get player's available positions
|
||||
function getPlayerPositions(player: SbaPlayer): string[] {
|
||||
const positions: string[] = []
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
const pos = player[`pos_${i}` as keyof SbaPlayer]
|
||||
if (pos && typeof pos === 'string') {
|
||||
positions.push(pos)
|
||||
}
|
||||
}
|
||||
// Always add DH as an option for all players
|
||||
if (!positions.includes('DH')) {
|
||||
positions.push('DH')
|
||||
}
|
||||
return positions
|
||||
}
|
||||
|
||||
// Drag handlers
|
||||
function handleRosterDrag(player: SbaPlayer, toSlotIndex: number, fromSlotIndex?: number) {
|
||||
const lineup = activeTab.value === 'home' ? homeLineup.value : awayLineup.value
|
||||
|
||||
// If dragging from another slot, swap or move
|
||||
if (fromSlotIndex !== undefined && fromSlotIndex !== toSlotIndex) {
|
||||
const fromSlot = lineup[fromSlotIndex]
|
||||
const toSlot = lineup[toSlotIndex]
|
||||
|
||||
// Swap players
|
||||
const tempPlayer = toSlot.player
|
||||
const tempPosition = toSlot.position
|
||||
|
||||
toSlot.player = fromSlot.player
|
||||
toSlot.position = fromSlot.position
|
||||
|
||||
fromSlot.player = tempPlayer
|
||||
fromSlot.position = tempPosition
|
||||
} else if (fromSlotIndex === undefined) {
|
||||
// Adding from roster pool
|
||||
lineup[toSlotIndex].player = player
|
||||
|
||||
// For pitcher slot (index 9), always use 'P'
|
||||
if (toSlotIndex === 9) {
|
||||
lineup[toSlotIndex].position = 'P'
|
||||
} else {
|
||||
// Auto-suggest first available position
|
||||
const availablePositions = getPlayerPositions(player)
|
||||
if (availablePositions.length > 0) {
|
||||
lineup[toSlotIndex].position = availablePositions[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removePlayer(slotIndex: number) {
|
||||
const lineup = activeTab.value === 'home' ? homeLineup.value : awayLineup.value
|
||||
lineup[slotIndex].player = null
|
||||
lineup[slotIndex].position = null
|
||||
}
|
||||
|
||||
// Fetch game data
|
||||
async function fetchGameData() {
|
||||
try {
|
||||
const response = await fetch(`${config.public.apiUrl}/api/games/${gameId.value}`)
|
||||
const data = await response.json()
|
||||
homeTeamId.value = data.home_team_id
|
||||
awayTeamId.value = data.away_team_id
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch game data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch roster for a team
|
||||
async function fetchRoster(teamId: number) {
|
||||
try {
|
||||
loadingRoster.value = true
|
||||
const response = await fetch(`${config.public.apiUrl}/api/teams/${teamId}/roster?season=${season.value}`)
|
||||
const data = await response.json()
|
||||
return data.players as SbaPlayer[]
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch roster for team ${teamId}:`, error)
|
||||
return []
|
||||
} finally {
|
||||
loadingRoster.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Submit lineups
|
||||
async function submitLineups() {
|
||||
if (!canSubmit.value || submittingLineups.value) return
|
||||
|
||||
submittingLineups.value = true
|
||||
|
||||
// Build request
|
||||
const homeLineupRequest: LineupPlayerRequest[] = homeLineup.value
|
||||
.filter(s => s.player)
|
||||
.map(s => ({
|
||||
player_id: s.player!.id,
|
||||
position: s.position!,
|
||||
batting_order: s.battingOrder
|
||||
}))
|
||||
|
||||
const awayLineupRequest: LineupPlayerRequest[] = awayLineup.value
|
||||
.filter(s => s.player)
|
||||
.map(s => ({
|
||||
player_id: s.player!.id,
|
||||
position: s.position!,
|
||||
batting_order: s.battingOrder
|
||||
}))
|
||||
|
||||
const request: SubmitLineupsRequest = {
|
||||
home_lineup: homeLineupRequest,
|
||||
away_lineup: awayLineupRequest
|
||||
}
|
||||
|
||||
console.log('Submitting lineup request:', JSON.stringify(request, null, 2))
|
||||
|
||||
try {
|
||||
const response = await fetch(`${config.public.apiUrl}/api/games/${gameId.value}/lineups`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(request)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
console.error('Lineup submission error:', error)
|
||||
|
||||
// Handle Pydantic validation errors
|
||||
if (error.detail && Array.isArray(error.detail)) {
|
||||
const messages = error.detail.map((err: any) => {
|
||||
if (err.loc) {
|
||||
const location = err.loc.join(' → ')
|
||||
return `${location}: ${err.msg}`
|
||||
}
|
||||
return err.msg || JSON.stringify(err)
|
||||
})
|
||||
throw new Error(`Validation errors:\n${messages.join('\n')}`)
|
||||
}
|
||||
|
||||
throw new Error(typeof error.detail === 'string' ? error.detail : JSON.stringify(error.detail))
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
console.log('Lineups submitted:', result)
|
||||
|
||||
// Redirect to game page
|
||||
router.push(`/games/${gameId.value}`)
|
||||
} catch (error) {
|
||||
console.error('Failed to submit lineups:', error)
|
||||
alert(error instanceof Error ? error.message : 'Failed to submit lineups')
|
||||
} finally {
|
||||
submittingLineups.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
onMounted(async () => {
|
||||
await fetchGameData()
|
||||
|
||||
if (homeTeamId.value) {
|
||||
homeRoster.value = await fetchRoster(homeTeamId.value)
|
||||
}
|
||||
if (awayTeamId.value) {
|
||||
awayRoster.value = await fetchRoster(awayTeamId.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-900 text-white p-4">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl font-bold mb-2">Build Your Lineup</h1>
|
||||
<p class="text-gray-400">Drag players from the roster to the lineup slots</p>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="flex gap-2 mb-6 border-b border-gray-700">
|
||||
<button
|
||||
@click="activeTab = 'away'"
|
||||
:class="[
|
||||
'px-6 py-3 font-semibold transition-colors',
|
||||
activeTab === 'away'
|
||||
? 'bg-blue-600 text-white border-b-2 border-blue-500'
|
||||
: 'text-gray-400 hover:text-white'
|
||||
]"
|
||||
>
|
||||
Away Lineup
|
||||
</button>
|
||||
<button
|
||||
@click="activeTab = 'home'"
|
||||
:class="[
|
||||
'px-6 py-3 font-semibold transition-colors',
|
||||
activeTab === 'home'
|
||||
? 'bg-blue-600 text-white border-b-2 border-blue-500'
|
||||
: 'text-gray-400 hover:text-white'
|
||||
]"
|
||||
>
|
||||
Home Lineup
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="loadingRoster" class="text-center py-12">
|
||||
<div class="text-xl">Loading roster...</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<div v-else class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Roster Pool (Sticky on desktop) -->
|
||||
<div class="lg:col-span-1">
|
||||
<div class="lg:sticky lg:top-4">
|
||||
<h2 class="text-xl font-bold mb-4">Available Players</h2>
|
||||
<div class="bg-gray-800 rounded-lg p-4 space-y-2 max-h-[calc(100vh-8rem)] overflow-y-auto">
|
||||
<div
|
||||
v-for="player in currentRoster"
|
||||
:key="player.id"
|
||||
@dragstart="(e) => e.dataTransfer?.setData('player', JSON.stringify(player))"
|
||||
draggable="true"
|
||||
class="bg-gray-700 rounded p-3 cursor-move hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
<div class="font-semibold">{{ player.name }}</div>
|
||||
<div class="text-sm text-gray-400">
|
||||
{{ getPlayerPositions(player).join(', ') || 'No positions' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentRoster.length === 0" class="text-gray-500 text-center py-4">
|
||||
All players assigned
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lineup Slots -->
|
||||
<div class="lg:col-span-2">
|
||||
<h2 class="text-xl font-bold mb-4">Lineup</h2>
|
||||
|
||||
<!-- Batting order slots (1-9) -->
|
||||
<div class="space-y-3 mb-6">
|
||||
<div
|
||||
v-for="(slot, index) in currentLineup.slice(0, 9)"
|
||||
:key="index"
|
||||
class="bg-gray-800 rounded-lg p-4"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- Batting order number -->
|
||||
<div class="text-2xl font-bold text-gray-500 w-8">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
|
||||
<!-- Player slot -->
|
||||
<div
|
||||
class="flex-1"
|
||||
@drop.prevent="(e) => {
|
||||
const playerData = e.dataTransfer?.getData('player')
|
||||
const fromSlotData = e.dataTransfer?.getData('fromSlot')
|
||||
if (playerData) {
|
||||
const player = JSON.parse(playerData) as SbaPlayer
|
||||
const fromSlot = fromSlotData ? parseInt(fromSlotData) : undefined
|
||||
handleRosterDrag(player, index, fromSlot)
|
||||
}
|
||||
}"
|
||||
@dragover.prevent
|
||||
>
|
||||
<div
|
||||
v-if="slot.player"
|
||||
class="bg-blue-900 rounded p-3 cursor-move"
|
||||
draggable="true"
|
||||
@dragstart="(e) => {
|
||||
e.dataTransfer?.setData('player', JSON.stringify(slot.player))
|
||||
e.dataTransfer?.setData('fromSlot', index.toString())
|
||||
}"
|
||||
>
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold">{{ slot.player.name }}</div>
|
||||
<div class="text-sm text-gray-400 mt-1">
|
||||
Available: {{ getPlayerPositions(slot.player).join(', ') }}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="removePlayer(index)"
|
||||
class="text-red-400 hover:text-red-300 ml-2"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="border-2 border-dashed border-gray-600 rounded p-4 text-center text-gray-500">
|
||||
Drop player here
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Position selector -->
|
||||
<div class="w-32">
|
||||
<select
|
||||
v-if="slot.player"
|
||||
v-model="slot.position"
|
||||
:class="[
|
||||
'w-full bg-gray-700 border rounded px-3 py-2',
|
||||
duplicatePositions.includes(slot.position || '')
|
||||
? 'border-red-500 font-bold text-red-400'
|
||||
: 'border-gray-600'
|
||||
]"
|
||||
>
|
||||
<option :value="null">Position</option>
|
||||
<option
|
||||
v-for="pos in getPlayerPositions(slot.player)"
|
||||
:key="pos"
|
||||
:value="pos"
|
||||
>
|
||||
{{ pos }}
|
||||
</option>
|
||||
</select>
|
||||
<div v-else class="text-gray-600 text-sm text-center">
|
||||
-
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pitcher slot (10) -->
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-semibold mb-3 flex items-center gap-2">
|
||||
Pitcher (Non-Batting)
|
||||
<span v-if="pitcherSlotDisabled" class="text-sm text-yellow-400">
|
||||
(Disabled - Pitcher batting)
|
||||
</span>
|
||||
</h3>
|
||||
<div
|
||||
:class="[
|
||||
'bg-gray-800 rounded-lg p-4',
|
||||
pitcherSlotDisabled && 'opacity-50 pointer-events-none'
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="text-2xl font-bold text-gray-500 w-8">P</div>
|
||||
|
||||
<div
|
||||
class="flex-1"
|
||||
@drop.prevent="(e) => {
|
||||
const playerData = e.dataTransfer?.getData('player')
|
||||
const fromSlotData = e.dataTransfer?.getData('fromSlot')
|
||||
if (playerData) {
|
||||
const player = JSON.parse(playerData) as SbaPlayer
|
||||
const fromSlot = fromSlotData ? parseInt(fromSlotData) : undefined
|
||||
handleRosterDrag(player, 9, fromSlot)
|
||||
}
|
||||
}"
|
||||
@dragover.prevent
|
||||
>
|
||||
<div
|
||||
v-if="currentLineup[9].player"
|
||||
class="bg-blue-900 rounded p-3 cursor-move"
|
||||
draggable="true"
|
||||
@dragstart="(e) => {
|
||||
e.dataTransfer?.setData('player', JSON.stringify(currentLineup[9].player))
|
||||
e.dataTransfer?.setData('fromSlot', '9')
|
||||
}"
|
||||
>
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold">{{ currentLineup[9].player.name }}</div>
|
||||
<div class="text-sm text-gray-400 mt-1">
|
||||
Available: {{ getPlayerPositions(currentLineup[9].player).join(', ') }}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="removePlayer(9)"
|
||||
class="text-red-400 hover:text-red-300 ml-2"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="border-2 border-dashed border-gray-600 rounded p-4 text-center text-gray-500">
|
||||
Drop pitcher here
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-32">
|
||||
<div class="text-gray-600 text-sm text-center font-semibold">P</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Validation errors -->
|
||||
<div v-if="validationErrors.length > 0" class="bg-red-900/30 border border-red-500 rounded-lg p-4 mb-4">
|
||||
<div class="font-semibold mb-2">Please fix the following errors:</div>
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li v-for="error in validationErrors" :key="error" class="text-sm">
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Submit button -->
|
||||
<ActionButton
|
||||
variant="success"
|
||||
size="lg"
|
||||
full-width
|
||||
:disabled="!canSubmit"
|
||||
:loading="submittingLineups"
|
||||
@click="submitLineups"
|
||||
>
|
||||
{{ submittingLineups ? 'Submitting Lineups...' : (canSubmit ? 'Submit Lineups & Start Game' : 'Complete Both Lineups to Continue') }}
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
21
frontend-sba/plugins/auth.client.ts
Normal file
21
frontend-sba/plugins/auth.client.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Auth Plugin - Client Side Only
|
||||
*
|
||||
* Initializes authentication state from localStorage before any navigation occurs.
|
||||
* This ensures the auth middleware has the correct state when checking authentication.
|
||||
*/
|
||||
|
||||
import { useAuthStore } from '~/store/auth'
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// Initialize auth from localStorage on app load
|
||||
authStore.initializeAuth()
|
||||
|
||||
console.log('[Auth Plugin] Initialized auth state:', {
|
||||
isAuthenticated: authStore.isAuthenticated,
|
||||
isTokenValid: authStore.isTokenValid,
|
||||
hasUser: !!authStore.currentUser
|
||||
})
|
||||
})
|
||||
@ -86,6 +86,28 @@ export interface Team {
|
||||
league_id: 'sba' | 'pd'
|
||||
}
|
||||
|
||||
/**
|
||||
* SBA Team data (from /api/teams endpoint)
|
||||
*/
|
||||
export interface SbaTeam {
|
||||
id: number
|
||||
abbrev: string
|
||||
sname: string // Short name (e.g., "Geese")
|
||||
lname: string // Long name (e.g., "Everett Geese")
|
||||
color: string // Hex color code
|
||||
manager_legacy: string
|
||||
gmid: string | null
|
||||
gmid2: string | null
|
||||
division: {
|
||||
id: number
|
||||
division_name: string
|
||||
division_abbrev: string
|
||||
league_name: string
|
||||
league_abbrev: string
|
||||
season: number
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User's teams response
|
||||
*/
|
||||
@ -177,3 +199,24 @@ export interface RefreshTokenResponse {
|
||||
access_token: string
|
||||
expires_in: number
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/games/:id/lineups
|
||||
*/
|
||||
export interface LineupPlayerRequest {
|
||||
player_id: number
|
||||
position: string
|
||||
batting_order: number | null // null for pitcher in DH lineup
|
||||
}
|
||||
|
||||
export interface SubmitLineupsRequest {
|
||||
home_lineup: LineupPlayerRequest[] // 9-10 players
|
||||
away_lineup: LineupPlayerRequest[] // 9-10 players
|
||||
}
|
||||
|
||||
export interface SubmitLineupsResponse {
|
||||
game_id: string
|
||||
message: string
|
||||
home_lineup_count: number
|
||||
away_lineup_count: number
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user