Vue Guide - RumenDamyanov/js-chess GitHub Wiki

Vue.js Guide

Learn how to build chess applications using Vue.js with the JS Chess project, leveraging the progressive framework's reactive data binding and component system.

Overview

The Vue.js implementation demonstrates how to use the progressive framework to build interactive chess applications. It showcases Vue's reactive data system, component composition, and modern development patterns with Vite.

Project Structure

vue/
├── package.json            # Dependencies and scripts
├── vite.config.js          # Vite build configuration
├── index.html              # HTML entry point
├── src/
│   ├── main.js             # Application entry point
│   ├── App.vue             # Root component
│   ├── components/         # Vue components
│   │   ├── ChessBoard.vue  # Chess board component
│   │   ├── GameControls.vue # Game control buttons
│   │   ├── GameStatus.vue  # Status display
│   │   ├── MoveHistory.vue # Move history component
│   │   └── SettingsModal.vue # Settings dialog
│   ├── composables/        # Vue 3 composables
│   │   ├── useChessGame.js # Game logic composable
│   │   ├── useWebSocket.js # WebSocket composable
│   │   └── useSettings.js  # Settings composable
│   ├── services/           # API services
│   │   └── chessApi.js     # API client
│   ├── assets/             # Static assets
│   └── styles/             # Component styles
│       └── chess.css       # Chess-specific styles
└── public/                 # Public assets
    └── pieces/             # Chess piece images

Getting Started

1. Setup Development Environment

# Clone the repository
git clone --recursive https://github.com/RumenDamyanov/js-chess.git
cd js-chess

# Start the Vue.js application
make run-vue

# Or manually
cd vue
npm install
npm run dev

2. Open in Browser

Visit http://localhost:3003 to see the Vue.js chess application in action.

Core Implementation

Main Application Setup

The main.js file bootstraps the Vue application:

// main.js - Vue application entry point
import { createApp } from 'vue'
import App from './App.vue'
import './styles/chess.css'

const app = createApp(App)

// Global error handler
app.config.errorHandler = (err, vm, info) => {
  console.error('Vue error:', err, info)
}

// Global properties for API base URL
app.config.globalProperties.$apiUrl = 'http://localhost:8080'

app.mount('#app')

Root Component

The App.vue component serves as the application shell:

<template>
  <div id="app">
    <header class="app-header">
      <nav class="app-nav">
        <div class="nav-brand">
          <h1>JS Chess</h1>
          <span class="framework-badge">Vue.js</span>
        </div>
        <div class="nav-links">
          <a href="/">Home</a>
          <a href="/vanilla-js">Vanilla JS</a>
          <a href="/jquery">jQuery</a>
          <a href="/vue" class="active">Vue.js</a>
          <a href="/react">React</a>
          <a href="/angular">Angular</a>
        </div>
      </nav>
    </header>

    <main class="container">
      <div class="game-container">
        <ChessBoard
          :board="gameState.board"
          :selected-square="selectedSquare"
          :valid-moves="validMoves"
          :last-move="gameState.lastMove"
          @square-click="handleSquareClick"
          @piece-drag="handlePieceDrag"
        />

        <div class="game-sidebar">
          <GameControls
            :is-player-turn="isPlayerTurn"
            :can-undo="canUndoMove"
            @new-game="createNewGame"
            @undo-move="undoLastMove"
            @get-hint="getHint"
            @show-settings="showSettings = true"
          />

          <GameStatus
            :status="gameStatus"
            :current-player="currentPlayer"
            :in-check="gameState.inCheck"
            :game-over="gameState.gameOver"
          />

          <MoveHistory
            :moves="moveHistory"
            :current-move="currentMoveIndex"
            @jump-to-move="jumpToMove"
          />
        </div>
      </div>
    </main>

    <SettingsModal
      v-if="showSettings"
      :settings="settings"
      @save="saveSettings"
      @close="showSettings = false"
    />

    <div v-if="error" class="error-message">
      {{ error }}
      <button @click="error = null" class="close-btn">&times;</button>
    </div>
  </div>
</template>

<script>
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import ChessBoard from './components/ChessBoard.vue'
import GameControls from './components/GameControls.vue'
import GameStatus from './components/GameStatus.vue'
import MoveHistory from './components/MoveHistory.vue'
import SettingsModal from './components/SettingsModal.vue'
import { useChessGame } from './composables/useChessGame'
import { useWebSocket } from './composables/useWebSocket'
import { useSettings } from './composables/useSettings'

export default {
  name: 'App',
  components: {
    ChessBoard,
    GameControls,
    GameStatus,
    MoveHistory,
    SettingsModal
  },
  setup() {
    // Reactive state
    const showSettings = ref(false)
    const error = ref(null)

    // Composables
    const {
      gameState,
      selectedSquare,
      validMoves,
      moveHistory,
      currentMoveIndex,
      isPlayerTurn,
      gameStatus,
      currentPlayer,
      canUndoMove,
      createNewGame,
      makeMove,
      undoLastMove,
      getHint,
      jumpToMove,
      handleSquareClick,
      handlePieceDrag
    } = useChessGame()

    const { settings, saveSettings } = useSettings()

    const { connect, disconnect } = useWebSocket(gameState)

    // Error handling
    const showError = (message) => {
      error.value = message
      setTimeout(() => {
        error.value = null
      }, 5000)
    }

    // Lifecycle hooks
    onMounted(async () => {
      try {
        await createNewGame()
        connect()
      } catch (err) {
        showError('Failed to initialize game')
      }
    })

    onUnmounted(() => {
      disconnect()
    })

    return {
      // State
      showSettings,
      error,
      gameState,
      selectedSquare,
      validMoves,
      moveHistory,
      currentMoveIndex,
      isPlayerTurn,
      gameStatus,
      currentPlayer,
      canUndoMove,
      settings,

      // Methods
      createNewGame,
      undoLastMove,
      getHint,
      jumpToMove,
      handleSquareClick,
      handlePieceDrag,
      saveSettings,
      showError
    }
  }
}
</script>

<style>
@import '/shared/styles/common.css';

.game-container {
  display: flex;
  gap: 2rem;
  align-items: flex-start;
  justify-content: center;
  max-width: 1000px;
  margin: 0 auto;
  padding: 2rem;
}

.game-sidebar {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
  min-width: 300px;
}

.error-message {
  position: fixed;
  top: 20px;
  right: 20px;
  background-color: #f44336;
  color: white;
  padding: 1rem 1.5rem;
  border-radius: 6px;
  z-index: 1000;
  display: flex;
  align-items: center;
  gap: 1rem;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  animation: slideIn 0.3s ease-out;
}

@keyframes slideIn {
  from {
    transform: translateX(100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

.close-btn {
  background: none;
  border: none;
  color: white;
  font-size: 1.2rem;
  cursor: pointer;
  padding: 0;
}

@media (max-width: 768px) {
  .game-container {
    flex-direction: column;
    align-items: center;
  }

  .game-sidebar {
    width: 100%;
    max-width: 400px;
  }
}
</style>

Chess Board Component

The ChessBoard.vue component handles board rendering and interactions:

<template>
  <div class="chess-board" :class="boardTheme">
    <div
      v-for="(square, index) in squares"
      :key="square.position"
      :class="getSquareClasses(square)"
      :data-position="square.position"
      @click="$emit('squareClick', square.position)"
      @dragover.prevent
      @drop="handleDrop($event, square.position)"
    >
      <!-- Coordinate labels -->
      <span v-if="showCoordinates && square.isFileLabel" class="file-label">
        {{ square.file }}
      </span>
      <span v-if="showCoordinates && square.isRankLabel" class="rank-label">
        {{ square.rank }}
      </span>

      <!-- Chess piece -->
      <div
        v-if="square.piece"
        :class="['piece', { 'dragging': draggedPiece === square.position }]"
        :draggable="canDragPiece(square.piece)"
        @dragstart="handleDragStart($event, square.position)"
        @dragend="handleDragEnd"
      >
        {{ getPieceSymbol(square.piece) }}
      </div>

      <!-- Move indicator -->
      <div v-if="isValidMove(square.position)" class="move-indicator"></div>
    </div>
  </div>
</template>

<script>
import { ref, computed, watch } from 'vue'

export default {
  name: 'ChessBoard',
  emits: ['squareClick', 'pieceDrag'],
  props: {
    board: {
      type: String,
      default: ''
    },
    selectedSquare: {
      type: String,
      default: null
    },
    validMoves: {
      type: Array,
      default: () => []
    },
    lastMove: {
      type: Object,
      default: null
    },
    boardTheme: {
      type: String,
      default: 'classic'
    },
    showCoordinates: {
      type: Boolean,
      default: true
    }
  },
  setup(props, { emit }) {
    const draggedPiece = ref(null)

    const pieceSymbols = {
      'white': {
        'king': '', 'queen': '', 'rook': '',
        'bishop': '', 'knight': '', 'pawn': ''
      },
      'black': {
        'king': '', 'queen': '', 'rook': '',
        'bishop': '', 'knight': '', 'pawn': ''
      }
    }

    const squares = computed(() => {
      const squareArray = []

      for (let rank = 8; rank >= 1; rank--) {
        for (let file = 0; file < 8; file++) {
          const fileChar = String.fromCharCode(97 + file) // a-h
          const position = fileChar + rank

          squareArray.push({
            position,
            file: fileChar,
            rank,
            isLight: (rank + file) % 2 === 1,
            piece: getPieceAt(position),
            isFileLabel: rank === 1,
            isRankLabel: file === 0
          })
        }
      }

      return squareArray
    })

    const getPieceAt = (position) => {
      if (!props.board) return null

      // Parse board string and find piece at position
      // This is a simplified implementation - in real app, use proper FEN parsing
      return parseBoardString(props.board, position)
    }

    const parseBoardString = (boardString, position) => {
      // Implementation depends on board format from API
      // This is a placeholder - implement based on your API response format
      return null
    }

    const getPieceSymbol = (piece) => {
      if (!piece) return ''

      const color = piece.color || (piece === piece.toUpperCase() ? 'white' : 'black')
      const type = getPieceType(piece)

      return pieceSymbols[color]?.[type] || ''
    }

    const getPieceType = (piece) => {
      const types = {
        'k': 'king', 'q': 'queen', 'r': 'rook',
        'b': 'bishop', 'n': 'knight', 'p': 'pawn'
      }
      return types[piece.toLowerCase()] || 'pawn'
    }

    const getSquareClasses = (square) => {
      return [
        'square',
        square.isLight ? 'light' : 'dark',
        {
          'selected': props.selectedSquare === square.position,
          'valid-move': isValidMove(square.position),
          'last-move': isLastMove(square.position)
        }
      ]
    }

    const isValidMove = (position) => {
      return props.validMoves.includes(position)
    }

    const isLastMove = (position) => {
      if (!props.lastMove) return false
      return props.lastMove.from === position || props.lastMove.to === position
    }

    const canDragPiece = (piece) => {
      if (!piece) return false
      // Only allow dragging white pieces (player pieces)
      return piece.color === 'white' || piece === piece.toUpperCase()
    }

    const handleDragStart = (event, position) => {
      draggedPiece.value = position
      event.dataTransfer.setData('text/plain', position)
      event.dataTransfer.effectAllowed = 'move'

      emit('pieceDrag', { type: 'start', position })
    }

    const handleDragEnd = () => {
      draggedPiece.value = null
      emit('pieceDrag', { type: 'end' })
    }

    const handleDrop = (event, position) => {
      event.preventDefault()
      const fromPosition = event.dataTransfer.getData('text/plain')

      if (fromPosition && fromPosition !== position) {
        emit('squareClick', position, { isDrop: true, from: fromPosition })
      }
    }

    return {
      draggedPiece,
      squares,
      getSquareClasses,
      getPieceSymbol,
      isValidMove,
      canDragPiece,
      handleDragStart,
      handleDragEnd,
      handleDrop
    }
  }
}
</script>

<style scoped>
.chess-board {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  grid-template-rows: repeat(8, 1fr);
  width: 480px;
  height: 480px;
  border: 2px solid var(--border-color);
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

.square {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background-color 0.2s ease;
}

.square.light {
  background-color: #f0d9b5;
}

.square.dark {
  background-color: #b58863;
}

.square:hover {
  background-color: rgba(255, 255, 0, 0.3);
}

.square.selected {
  background-color: rgba(255, 255, 0, 0.5);
  box-shadow: inset 0 0 0 3px #ffff00;
}

.square.valid-move {
  background-color: rgba(0, 255, 0, 0.3);
}

.square.last-move {
  background-color: rgba(255, 255, 0, 0.4);
}

.piece {
  font-size: 36px;
  line-height: 1;
  cursor: grab;
  user-select: none;
  transition: transform 0.1s ease;
  z-index: 10;
}

.piece:hover {
  transform: scale(1.1);
}

.piece:active {
  cursor: grabbing;
}

.piece.dragging {
  opacity: 0.5;
}

.move-indicator {
  position: absolute;
  width: 20px;
  height: 20px;
  background-color: rgba(0, 255, 0, 0.6);
  border-radius: 50%;
  pointer-events: none;
}

.rank-label,
.file-label {
  position: absolute;
  font-size: 12px;
  font-weight: bold;
  color: rgba(0, 0, 0, 0.6);
  pointer-events: none;
}

.rank-label {
  top: 2px;
  left: 2px;
}

.file-label {
  bottom: 2px;
  right: 2px;
}

/* Board themes */
.chess-board.modern .square.light {
  background: linear-gradient(135deg, #f8f8f8, #e8e8e8);
}

.chess-board.modern .square.dark {
  background: linear-gradient(135deg, #8b7355, #6d5a3d);
}

.chess-board.wooden .square.light {
  background: linear-gradient(135deg, #deb887, #d2691e);
}

.chess-board.wooden .square.dark {
  background: linear-gradient(135deg, #8b4513, #654321);
}

@media (max-width: 768px) {
  .chess-board {
    width: 320px;
    height: 320px;
  }

  .piece {
    font-size: 24px;
  }
}
</style>

Game Logic Composable

The useChessGame.js composable manages game state and logic:

// composables/useChessGame.js
import { ref, reactive, computed } from 'vue'
import { chessApi } from '../services/chessApi'

export function useChessGame() {
  // Reactive state
  const gameState = reactive({
    id: null,
    board: '',
    status: 'ready',
    activeColor: 'white',
    inCheck: false,
    gameOver: false,
    winner: null,
    lastMove: null
  })

  const selectedSquare = ref(null)
  const validMoves = ref([])
  const moveHistory = ref([])
  const currentMoveIndex = ref(-1)
  const isPlayerTurn = ref(true)
  const isLoading = ref(false)

  // Computed properties
  const gameStatus = computed(() => {
    if (gameState.gameOver) {
      if (gameState.winner) {
        return `Game Over - ${gameState.winner} wins!`
      }
      return 'Game Over - Draw'
    }

    if (gameState.inCheck) {
      return `${gameState.activeColor} is in check`
    }

    return gameState.status
  })

  const currentPlayer = computed(() => {
    return gameState.activeColor === 'white' ? 'White' : 'Black'
  })

  const canUndoMove = computed(() => {
    return moveHistory.value.length > 0 && !gameState.gameOver
  })

  // Game actions
  const createNewGame = async () => {
    try {
      isLoading.value = true

      const response = await chessApi.createGame({
        ai_enabled: true,
        difficulty: 'medium'
      })

      Object.assign(gameState, response)

      // Reset state
      selectedSquare.value = null
      validMoves.value = []
      moveHistory.value = []
      currentMoveIndex.value = -1
      isPlayerTurn.value = true

      return response
    } catch (error) {
      console.error('Failed to create game:', error)
      throw error
    } finally {
      isLoading.value = false
    }
  }

  const makeMove = async (from, to) => {
    if (!isPlayerTurn.value || !gameState.id) return

    try {
      isPlayerTurn.value = false

      const response = await chessApi.makeMove(gameState.id, from, to)

      if (response.success) {
        // Update game state
        Object.assign(gameState, response.gameState)

        // Add to move history
        moveHistory.value.push(response.move)
        currentMoveIndex.value = moveHistory.value.length - 1

        // Clear selection
        clearSelection()

        // Check for game end
        if (response.gameState.gameOver) {
          return response
        }

        // Get AI move
        await makeAIMove()
      }

      return response
    } catch (error) {
      console.error('Failed to make move:', error)
      isPlayerTurn.value = true
      throw error
    }
  }

  const makeAIMove = async () => {
    try {
      const response = await chessApi.getAIMove(gameState.id)

      if (response.success) {
        Object.assign(gameState, response.gameState)
        moveHistory.value.push(response.move)
        currentMoveIndex.value = moveHistory.value.length - 1

        isPlayerTurn.value = true
      }

      return response
    } catch (error) {
      console.error('AI move failed:', error)
      isPlayerTurn.value = true
      throw error
    }
  }

  const undoLastMove = async () => {
    if (!canUndoMove.value) return

    try {
      isLoading.value = true

      const response = await chessApi.undoMove(gameState.id)

      if (response.success) {
        Object.assign(gameState, response.gameState)

        // Remove last two moves (player + AI)
        moveHistory.value = moveHistory.value.slice(0, -2)
        currentMoveIndex.value = moveHistory.value.length - 1

        isPlayerTurn.value = true
        clearSelection()
      }

      return response
    } catch (error) {
      console.error('Failed to undo move:', error)
      throw error
    } finally {
      isLoading.value = false
    }
  }

  const getHint = async () => {
    try {
      const response = await chessApi.getHint(gameState.id)
      return response
    } catch (error) {
      console.error('Failed to get hint:', error)
      throw error
    }
  }

  const jumpToMove = (moveIndex) => {
    currentMoveIndex.value = moveIndex
    // Implementation for jumping to specific move in history
  }

  const handleSquareClick = async (position, options = {}) => {
    if (!isPlayerTurn.value) return

    if (options.isDrop && options.from) {
      // Handle drag and drop
      if (validMoves.value.includes(position)) {
        await makeMove(options.from, position)
      }
      return
    }

    if (selectedSquare.value === position) {
      // Deselect if clicking same square
      clearSelection()
      return
    }

    if (selectedSquare.value && validMoves.value.includes(position)) {
      // Make move if valid target
      await makeMove(selectedSquare.value, position)
      return
    }

    // Select new square
    selectSquare(position)
  }

  const handlePieceDrag = ({ type, position }) => {
    if (type === 'start') {
      selectSquare(position)
    } else if (type === 'end') {
      // Keep selection for visual feedback
    }
  }

  const selectSquare = (position) => {
    selectedSquare.value = position

    // Get valid moves for this position
    validMoves.value = getValidMovesForPosition(position)
  }

  const clearSelection = () => {
    selectedSquare.value = null
    validMoves.value = []
  }

  const getValidMovesForPosition = (position) => {
    // This should come from the API in a real implementation
    // For now, return empty array
    return []
  }

  return {
    // State
    gameState,
    selectedSquare,
    validMoves,
    moveHistory,
    currentMoveIndex,
    isPlayerTurn,
    isLoading,

    // Computed
    gameStatus,
    currentPlayer,
    canUndoMove,

    // Actions
    createNewGame,
    makeMove,
    undoLastMove,
    getHint,
    jumpToMove,
    handleSquareClick,
    handlePieceDrag,
    selectSquare,
    clearSelection
  }
}

API Service

The chessApi.js service handles API communication:

// services/chessApi.js
const API_BASE_URL = 'http://localhost:8080'

class ChessApiError extends Error {
  constructor(message, status, response) {
    super(message)
    this.name = 'ChessApiError'
    this.status = status
    this.response = response
  }
}

const handleResponse = async (response) => {
  if (!response.ok) {
    const errorData = await response.json().catch(() => ({}))
    throw new ChessApiError(
      errorData.message || `HTTP ${response.status}`,
      response.status,
      errorData
    )
  }

  return response.json()
}

const apiRequest = async (endpoint, options = {}) => {
  const url = `${API_BASE_URL}${endpoint}`

  const config = {
    headers: {
      'Content-Type': 'application/json',
      ...options.headers
    },
    ...options
  }

  if (config.body && typeof config.body !== 'string') {
    config.body = JSON.stringify(config.body)
  }

  const response = await fetch(url, config)
  return handleResponse(response)
}

export const chessApi = {
  async createGame(options = {}) {
    return apiRequest('/api/games', {
      method: 'POST',
      body: {
        ai_enabled: true,
        difficulty: 'medium',
        ...options
      }
    })
  },

  async getGame(gameId) {
    return apiRequest(`/api/games/${gameId}`)
  },

  async makeMove(gameId, from, to, promotion = null) {
    return apiRequest(`/api/games/${gameId}/moves`, {
      method: 'POST',
      body: { from, to, promotion }
    })
  },

  async getAIMove(gameId) {
    return apiRequest(`/api/games/${gameId}/ai-move`, {
      method: 'POST'
    })
  },

  async undoMove(gameId) {
    return apiRequest(`/api/games/${gameId}/undo`, {
      method: 'POST'
    })
  },

  async getHint(gameId) {
    return apiRequest(`/api/games/${gameId}/hint`)
  },

  async getAnalysis(gameId) {
    return apiRequest(`/api/games/${gameId}/analysis`)
  },

  async saveGame(gameId) {
    return apiRequest(`/api/games/${gameId}/save`, {
      method: 'POST'
    })
  },

  async loadGame(gameId) {
    return apiRequest(`/api/games/${gameId}/load`)
  }
}

Advanced Features

WebSocket Integration

Create a WebSocket composable for real-time updates:

// composables/useWebSocket.js
import { ref, onUnmounted } from 'vue'

export function useWebSocket(gameState) {
  const ws = ref(null)
  const isConnected = ref(false)
  const reconnectAttempts = ref(0)
  const maxReconnectAttempts = 5

  const connect = () => {
    if (!gameState.id) return

    try {
      const wsUrl = `ws://localhost:8080/ws/games/${gameState.id}`
      ws.value = new WebSocket(wsUrl)

      ws.value.onopen = () => {
        isConnected.value = true
        reconnectAttempts.value = 0
        console.log('WebSocket connected')
      }

      ws.value.onmessage = (event) => {
        try {
          const data = JSON.parse(event.data)
          handleWebSocketMessage(data)
        } catch (error) {
          console.error('Failed to parse WebSocket message:', error)
        }
      }

      ws.value.onclose = () => {
        isConnected.value = false
        console.log('WebSocket disconnected')

        // Attempt reconnection
        if (reconnectAttempts.value < maxReconnectAttempts) {
          setTimeout(() => {
            reconnectAttempts.value++
            connect()
          }, 1000 * Math.pow(2, reconnectAttempts.value))
        }
      }

      ws.value.onerror = (error) => {
        console.error('WebSocket error:', error)
      }
    } catch (error) {
      console.error('Failed to create WebSocket connection:', error)
    }
  }

  const disconnect = () => {
    if (ws.value) {
      ws.value.close()
      ws.value = null
      isConnected.value = false
    }
  }

  const handleWebSocketMessage = (data) => {
    switch (data.type) {
      case 'game_update':
        Object.assign(gameState, data.gameState)
        break
      case 'move_made':
        // Handle move updates
        break
      case 'game_over':
        gameState.gameOver = true
        gameState.winner = data.winner
        break
      default:
        console.log('Unknown WebSocket message type:', data.type)
    }
  }

  onUnmounted(() => {
    disconnect()
  })

  return {
    isConnected,
    connect,
    disconnect
  }
}

Settings Management

Create a settings composable:

// composables/useSettings.js
import { reactive, watch } from 'vue'

const defaultSettings = {
  boardTheme: 'classic',
  showCoordinates: true,
  enableSounds: true,
  aiDifficulty: 'medium',
  autoPromoteQueen: false,
  animationSpeed: 'normal'
}

export function useSettings() {
  const settings = reactive({ ...defaultSettings })

  // Load settings from localStorage
  const loadSettings = () => {
    try {
      const saved = localStorage.getItem('vue-chess-settings')
      if (saved) {
        Object.assign(settings, JSON.parse(saved))
      }
    } catch (error) {
      console.error('Failed to load settings:', error)
    }
  }

  // Save settings to localStorage
  const saveSettings = (newSettings) => {
    try {
      Object.assign(settings, newSettings)
      localStorage.setItem('vue-chess-settings', JSON.stringify(settings))
    } catch (error) {
      console.error('Failed to save settings:', error)
    }
  }

  // Watch for changes and auto-save
  watch(settings, (newSettings) => {
    try {
      localStorage.setItem('vue-chess-settings', JSON.stringify(newSettings))
    } catch (error) {
      console.error('Failed to auto-save settings:', error)
    }
  }, { deep: true })

  // Initialize
  loadSettings()

  return {
    settings,
    saveSettings,
    loadSettings
  }
}

Component Examples

Game Controls Component

<template>
  <div class="game-controls">
    <button
      @click="$emit('newGame')"
      :disabled="isLoading"
      class="btn btn-primary"
    >
      <i class="icon-refresh"></i>
      New Game
    </button>

    <button
      @click="$emit('undoMove')"
      :disabled="!canUndo || isLoading"
      class="btn btn-secondary"
    >
      <i class="icon-undo"></i>
      Undo Move
    </button>

    <button
      @click="$emit('getHint')"
      :disabled="!isPlayerTurn || isLoading"
      class="btn btn-info"
    >
      <i class="icon-lightbulb"></i>
      Get Hint
    </button>

    <button
      @click="$emit('showSettings')"
      class="btn btn-outline"
    >
      <i class="icon-settings"></i>
      Settings
    </button>
  </div>
</template>

<script>
export default {
  name: 'GameControls',
  emits: ['newGame', 'undoMove', 'getHint', 'showSettings'],
  props: {
    isPlayerTurn: Boolean,
    canUndo: Boolean,
    isLoading: Boolean
  }
}
</script>

<style scoped>
.game-controls {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.btn {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.75rem 1rem;
  border: none;
  border-radius: 6px;
  font-size: 1rem;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.2s ease;
  text-align: left;
}

.btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.btn-primary {
  background: linear-gradient(135deg, var(--primary-color), var(--primary-color-dark));
  color: white;
}

.btn-secondary {
  background: linear-gradient(135deg, var(--gray-500), var(--gray-600));
  color: white;
}

.btn-info {
  background: linear-gradient(135deg, #17a2b8, #138496);
  color: white;
}

.btn-outline {
  background: transparent;
  border: 1px solid var(--border-color);
  color: var(--text-primary);
}

.btn:hover:not(:disabled) {
  transform: translateY(-1px);
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
</style>

Testing

Test your Vue.js implementation:

# Run Vue-specific tests
make test-vue

# Unit tests with Vitest
npm run test

# E2E tests with Cypress
npm run test:e2e

# Test checklist:
- Components render correctly
- Props and events work properly
- Composables manage state correctly
- API integration functions
- WebSocket connection works
- Responsive design adapts

Build and Deployment

Development Build

# Development server with hot reload
npm run dev

# Build for production
npm run build

# Preview production build
npm run preview

Vite Configuration

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  server: {
    port: 3003,
    host: true
  },
  build: {
    outDir: 'dist',
    sourcemap: true
  },
  resolve: {
    alias: {
      '@': '/src'
    }
  }
})

Performance Optimization

  1. Use v-memo for expensive renders
  2. Lazy load components with defineAsyncComponent
  3. Optimize reactivity with shallowRef
  4. Use v-once for static content
  5. Implement virtual scrolling for move history

Best Practices

  1. Composition API: Use composables for reusable logic
  2. Single File Components: Keep template, script, and style together
  3. Props Validation: Define proper prop types and validation
  4. Event Naming: Use kebab-case for event names
  5. Reactive State: Use reactive for objects, ref for primitives

Next Steps

  1. Review the API Integration Guide for backend communication
  2. Check the Chess Features Guide for advanced chess logic
  3. See the UI/UX Guide for design improvements
  4. Compare with other framework implementations

The Vue.js implementation demonstrates how modern reactive frameworks can create elegant, maintainable chess applications with excellent developer experience and performance.

⚠️ **GitHub.com Fallback** ⚠️