Vue Guide - RumenDamyanov/js-chess GitHub Wiki
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.
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.
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
# 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
Visit http://localhost:3003
to see the Vue.js chess application in action.
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')
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">×</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>
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>
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
}
}
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`)
}
}
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
}
}
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
}
}
<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>
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
# Development server with hot reload
npm run dev
# Build for production
npm run build
# Preview production build
npm run preview
// 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'
}
}
})
- Use
v-memo
for expensive renders - Lazy load components with
defineAsyncComponent
- Optimize reactivity with
shallowRef
- Use
v-once
for static content - Implement virtual scrolling for move history
- Composition API: Use composables for reusable logic
- Single File Components: Keep template, script, and style together
- Props Validation: Define proper prop types and validation
- Event Naming: Use kebab-case for event names
-
Reactive State: Use
reactive
for objects,ref
for primitives
- Review the API Integration Guide for backend communication
- Check the Chess Features Guide for advanced chess logic
- See the UI/UX Guide for design improvements
- 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.