Visual edit in Lovable
Edited UI in Lovable
This commit is contained in:
parent
85b730946a
commit
8711a69ee2
1 changed files with 42 additions and 133 deletions
|
|
@ -6,32 +6,25 @@ import { RefreshCw, RotateCcw, ExternalLink, ArrowLeft } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import SocialShare from "@/components/SocialShare";
|
import SocialShare from "@/components/SocialShare";
|
||||||
import { useSearchParams, useNavigate } from "react-router-dom";
|
import { useSearchParams, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
type Piece = 'white' | 'black' | null;
|
type Piece = 'white' | 'black' | null;
|
||||||
type Position = { row: number; col: number };
|
type Position = {
|
||||||
|
row: number;
|
||||||
|
col: number;
|
||||||
|
};
|
||||||
interface KnightsPuzzleProps {
|
interface KnightsPuzzleProps {
|
||||||
showSocialShare?: boolean;
|
showSocialShare?: boolean;
|
||||||
}
|
}
|
||||||
|
const KnightsPuzzle = ({
|
||||||
const KnightsPuzzle = ({ showSocialShare = false }: KnightsPuzzleProps) => {
|
showSocialShare = false
|
||||||
|
}: KnightsPuzzleProps) => {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const isFromPuzzles = searchParams.get('from') === 'puzzles';
|
const isFromPuzzles = searchParams.get('from') === 'puzzles';
|
||||||
// Initial state: white knights at top corners, black knights at bottom corners
|
// Initial state: white knights at top corners, black knights at bottom corners
|
||||||
const initialBoard: Piece[][] = [
|
const initialBoard: Piece[][] = [['white', null, 'white'], [null, null, null], ['black', null, 'black']];
|
||||||
['white', null, 'white'],
|
|
||||||
[null, null, null],
|
|
||||||
['black', null, 'black']
|
|
||||||
];
|
|
||||||
|
|
||||||
// Target state: black knights at top corners, white knights at bottom corners
|
// Target state: black knights at top corners, white knights at bottom corners
|
||||||
const targetBoard: Piece[][] = [
|
const targetBoard: Piece[][] = [['black', null, 'black'], [null, null, null], ['white', null, 'white']];
|
||||||
['black', null, 'black'],
|
|
||||||
[null, null, null],
|
|
||||||
['white', null, 'white']
|
|
||||||
];
|
|
||||||
|
|
||||||
const [board, setBoard] = useState<Piece[][]>(initialBoard);
|
const [board, setBoard] = useState<Piece[][]>(initialBoard);
|
||||||
const [selectedSquare, setSelectedSquare] = useState<Position | null>(null);
|
const [selectedSquare, setSelectedSquare] = useState<Position | null>(null);
|
||||||
const [moveCount, setMoveCount] = useState(0);
|
const [moveCount, setMoveCount] = useState(0);
|
||||||
|
|
@ -39,37 +32,29 @@ const KnightsPuzzle = ({ showSocialShare = false }: KnightsPuzzleProps) => {
|
||||||
const [moveHistory, setMoveHistory] = useState<Position[][]>([]);
|
const [moveHistory, setMoveHistory] = useState<Position[][]>([]);
|
||||||
|
|
||||||
// Knight move offsets
|
// Knight move offsets
|
||||||
const knightMoves = [
|
const knightMoves = [[-2, -1], [-2, 1], [-1, -2], [-1, 2], [1, -2], [1, 2], [2, -1], [2, 1]];
|
||||||
[-2, -1], [-2, 1], [-1, -2], [-1, 2],
|
|
||||||
[1, -2], [1, 2], [2, -1], [2, 1]
|
|
||||||
];
|
|
||||||
|
|
||||||
const isValidPosition = (row: number, col: number): boolean => {
|
const isValidPosition = (row: number, col: number): boolean => {
|
||||||
return row >= 0 && row < 3 && col >= 0 && col < 3;
|
return row >= 0 && row < 3 && col >= 0 && col < 3;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getValidMoves = (fromRow: number, fromCol: number): Position[] => {
|
const getValidMoves = (fromRow: number, fromCol: number): Position[] => {
|
||||||
const validMoves: Position[] = [];
|
const validMoves: Position[] = [];
|
||||||
|
|
||||||
knightMoves.forEach(([dRow, dCol]) => {
|
knightMoves.forEach(([dRow, dCol]) => {
|
||||||
const newRow = fromRow + dRow;
|
const newRow = fromRow + dRow;
|
||||||
const newCol = fromCol + dCol;
|
const newCol = fromCol + dCol;
|
||||||
|
|
||||||
if (isValidPosition(newRow, newCol) && board[newRow][newCol] === null) {
|
if (isValidPosition(newRow, newCol) && board[newRow][newCol] === null) {
|
||||||
validMoves.push({ row: newRow, col: newCol });
|
validMoves.push({
|
||||||
|
row: newRow,
|
||||||
|
col: newCol
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return validMoves;
|
return validMoves;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isValidMove = (from: Position, to: Position): boolean => {
|
const isValidMove = (from: Position, to: Position): boolean => {
|
||||||
const rowDiff = Math.abs(from.row - to.row);
|
const rowDiff = Math.abs(from.row - to.row);
|
||||||
const colDiff = Math.abs(from.col - to.col);
|
const colDiff = Math.abs(from.col - to.col);
|
||||||
|
return rowDiff === 2 && colDiff === 1 || rowDiff === 1 && colDiff === 2;
|
||||||
return (rowDiff === 2 && colDiff === 1) || (rowDiff === 1 && colDiff === 2);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkWinCondition = (currentBoard: Piece[][]): boolean => {
|
const checkWinCondition = (currentBoard: Piece[][]): boolean => {
|
||||||
for (let row = 0; row < 3; row++) {
|
for (let row = 0; row < 3; row++) {
|
||||||
for (let col = 0; col < 3; col++) {
|
for (let col = 0; col < 3; col++) {
|
||||||
|
|
@ -80,32 +65,32 @@ const KnightsPuzzle = ({ showSocialShare = false }: KnightsPuzzleProps) => {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSquareClick = (row: number, col: number) => {
|
const handleSquareClick = (row: number, col: number) => {
|
||||||
const piece = board[row][col];
|
const piece = board[row][col];
|
||||||
|
|
||||||
if (selectedSquare) {
|
if (selectedSquare) {
|
||||||
const from = selectedSquare;
|
const from = selectedSquare;
|
||||||
const to = { row, col };
|
const to = {
|
||||||
|
row,
|
||||||
|
col
|
||||||
|
};
|
||||||
|
|
||||||
// If clicking on the same square, deselect
|
// If clicking on the same square, deselect
|
||||||
if (from.row === to.row && from.col === to.col) {
|
if (from.row === to.row && from.col === to.col) {
|
||||||
setSelectedSquare(null);
|
setSelectedSquare(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If clicking on an empty square and it's a valid knight move
|
// If clicking on an empty square and it's a valid knight move
|
||||||
if (piece === null && isValidMove(from, to)) {
|
if (piece === null && isValidMove(from, to)) {
|
||||||
const newBoard = board.map(r => [...r]);
|
const newBoard = board.map(r => [...r]);
|
||||||
const movingPiece = newBoard[from.row][from.col];
|
const movingPiece = newBoard[from.row][from.col];
|
||||||
newBoard[from.row][from.col] = null;
|
newBoard[from.row][from.col] = null;
|
||||||
newBoard[to.row][to.col] = movingPiece;
|
newBoard[to.row][to.col] = movingPiece;
|
||||||
|
|
||||||
setBoard(newBoard);
|
setBoard(newBoard);
|
||||||
setMoveCount(moveCount + 1);
|
setMoveCount(moveCount + 1);
|
||||||
setMoveHistory([...moveHistory, [from, to]]);
|
setMoveHistory([...moveHistory, [from, to]]);
|
||||||
setSelectedSquare(null);
|
setSelectedSquare(null);
|
||||||
|
|
||||||
// Check win condition
|
// Check win condition
|
||||||
if (checkWinCondition(newBoard)) {
|
if (checkWinCondition(newBoard)) {
|
||||||
setIsComplete(true);
|
setIsComplete(true);
|
||||||
|
|
@ -113,16 +98,21 @@ const KnightsPuzzle = ({ showSocialShare = false }: KnightsPuzzleProps) => {
|
||||||
}
|
}
|
||||||
} else if (piece !== null) {
|
} else if (piece !== null) {
|
||||||
// Select a different piece
|
// Select a different piece
|
||||||
setSelectedSquare({ row, col });
|
setSelectedSquare({
|
||||||
|
row,
|
||||||
|
col
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
setSelectedSquare(null);
|
setSelectedSquare(null);
|
||||||
}
|
}
|
||||||
} else if (piece !== null) {
|
} else if (piece !== null) {
|
||||||
// Select a piece
|
// Select a piece
|
||||||
setSelectedSquare({ row, col });
|
setSelectedSquare({
|
||||||
|
row,
|
||||||
|
col
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetPuzzle = () => {
|
const resetPuzzle = () => {
|
||||||
setBoard(initialBoard);
|
setBoard(initialBoard);
|
||||||
setSelectedSquare(null);
|
setSelectedSquare(null);
|
||||||
|
|
@ -130,42 +120,33 @@ const KnightsPuzzle = ({ showSocialShare = false }: KnightsPuzzleProps) => {
|
||||||
setIsComplete(false);
|
setIsComplete(false);
|
||||||
setMoveHistory([]);
|
setMoveHistory([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const undoLastMove = () => {
|
const undoLastMove = () => {
|
||||||
if (moveHistory.length === 0) return;
|
if (moveHistory.length === 0) return;
|
||||||
|
|
||||||
const lastMove = moveHistory[moveHistory.length - 1];
|
const lastMove = moveHistory[moveHistory.length - 1];
|
||||||
const [from, to] = lastMove;
|
const [from, to] = lastMove;
|
||||||
|
|
||||||
const newBoard = board.map(r => [...r]);
|
const newBoard = board.map(r => [...r]);
|
||||||
const movingPiece = newBoard[to.row][to.col];
|
const movingPiece = newBoard[to.row][to.col];
|
||||||
newBoard[to.row][to.col] = null;
|
newBoard[to.row][to.col] = null;
|
||||||
newBoard[from.row][from.col] = movingPiece;
|
newBoard[from.row][from.col] = movingPiece;
|
||||||
|
|
||||||
setBoard(newBoard);
|
setBoard(newBoard);
|
||||||
setMoveCount(moveCount - 1);
|
setMoveCount(moveCount - 1);
|
||||||
setMoveHistory(moveHistory.slice(0, -1));
|
setMoveHistory(moveHistory.slice(0, -1));
|
||||||
setSelectedSquare(null);
|
setSelectedSquare(null);
|
||||||
setIsComplete(false);
|
setIsComplete(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSquareClass = (row: number, col: number): string => {
|
const getSquareClass = (row: number, col: number): string => {
|
||||||
const baseClass = "w-16 h-16 border-2 border-outline flex items-center justify-center cursor-pointer transition-all duration-200";
|
const baseClass = "w-16 h-16 border-2 border-outline flex items-center justify-center cursor-pointer transition-all duration-200";
|
||||||
const isSelected = selectedSquare?.row === row && selectedSquare?.col === col;
|
const isSelected = selectedSquare?.row === row && selectedSquare?.col === col;
|
||||||
const validMoves = selectedSquare ? getValidMoves(selectedSquare.row, selectedSquare.col) : [];
|
const validMoves = selectedSquare ? getValidMoves(selectedSquare.row, selectedSquare.col) : [];
|
||||||
const isValidMoveTarget = validMoves.some(move => move.row === row && move.col === col);
|
const isValidMoveTarget = validMoves.some(move => move.row === row && move.col === col);
|
||||||
|
|
||||||
let bgClass = (row + col) % 2 === 0 ? "bg-surface" : "bg-surface-variant";
|
let bgClass = (row + col) % 2 === 0 ? "bg-surface" : "bg-surface-variant";
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
bgClass = "bg-accent";
|
bgClass = "bg-accent";
|
||||||
} else if (isValidMoveTarget) {
|
} else if (isValidMoveTarget) {
|
||||||
bgClass = "bg-accent/30 ring-2 ring-accent";
|
bgClass = "bg-accent/30 ring-2 ring-accent";
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${baseClass} ${bgClass}`;
|
return `${baseClass} ${bgClass}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderPiece = (piece: Piece) => {
|
const renderPiece = (piece: Piece) => {
|
||||||
if (piece === 'white') {
|
if (piece === 'white') {
|
||||||
return <span className="text-4xl">♘</span>;
|
return <span className="text-4xl">♘</span>;
|
||||||
|
|
@ -174,21 +155,14 @@ const KnightsPuzzle = ({ showSocialShare = false }: KnightsPuzzleProps) => {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
return <div className="max-w-4xl mx-auto p-6 space-y-6">
|
||||||
return (
|
|
||||||
<div className="max-w-4xl mx-auto p-6 space-y-6">
|
|
||||||
{/* Back to Puzzles button - only show when coming from puzzles */}
|
{/* Back to Puzzles button - only show when coming from puzzles */}
|
||||||
{isFromPuzzles && (
|
{isFromPuzzles && <div className="flex items-center gap-4">
|
||||||
<div className="flex items-center gap-4">
|
<button onClick={() => navigate('/themes/puzzles')} className="text-primary hover:text-primary/80 font-medium flex items-center gap-2">
|
||||||
<button
|
|
||||||
onClick={() => navigate('/themes/puzzles')}
|
|
||||||
className="text-primary hover:text-primary/80 font-medium flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-4 h-4" />
|
<ArrowLeft className="w-4 h-4" />
|
||||||
Go back to Puzzles
|
Go back to Puzzles
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>}
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Green: Title and Description - Centered */}
|
{/* Green: Title and Description - Centered */}
|
||||||
<div className="space-y-2 text-center">
|
<div className="space-y-2 text-center">
|
||||||
|
|
@ -214,17 +188,9 @@ const KnightsPuzzle = ({ showSocialShare = false }: KnightsPuzzleProps) => {
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<div className="flex flex-col items-center gap-4">
|
<div className="flex flex-col items-center gap-4">
|
||||||
<div className="grid grid-cols-3 gap-1 border-2 border-outline rounded-lg p-4 bg-background">
|
<div className="grid grid-cols-3 gap-1 border-2 border-outline rounded-lg p-4 bg-background">
|
||||||
{board.map((row, rowIndex) =>
|
{board.map((row, rowIndex) => row.map((piece, colIndex) => <div key={`${rowIndex}-${colIndex}`} className={getSquareClass(rowIndex, colIndex)} onClick={() => handleSquareClick(rowIndex, colIndex)}>
|
||||||
row.map((piece, colIndex) => (
|
|
||||||
<div
|
|
||||||
key={`${rowIndex}-${colIndex}`}
|
|
||||||
className={getSquareClass(rowIndex, colIndex)}
|
|
||||||
onClick={() => handleSquareClick(rowIndex, colIndex)}
|
|
||||||
>
|
|
||||||
{renderPiece(piece)}
|
{renderPiece(piece)}
|
||||||
</div>
|
</div>))}
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Controls */}
|
{/* Controls */}
|
||||||
|
|
@ -233,12 +199,7 @@ const KnightsPuzzle = ({ showSocialShare = false }: KnightsPuzzleProps) => {
|
||||||
<RefreshCw className="w-4 h-4 mr-2" />
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button onClick={undoLastMove} variant="outline" size="sm" disabled={moveHistory.length === 0}>
|
||||||
onClick={undoLastMove}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
disabled={moveHistory.length === 0}
|
|
||||||
>
|
|
||||||
<RotateCcw className="w-4 h-4 mr-2" />
|
<RotateCcw className="w-4 h-4 mr-2" />
|
||||||
Undo
|
Undo
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -249,49 +210,9 @@ const KnightsPuzzle = ({ showSocialShare = false }: KnightsPuzzleProps) => {
|
||||||
|
|
||||||
{/* Purple: Initial and Target positions side by side */}
|
{/* Purple: Initial and Target positions side by side */}
|
||||||
<div className="grid grid-cols-2 gap-6">
|
<div className="grid grid-cols-2 gap-6">
|
||||||
<Card className="bg-blue-50/50 border-blue-200 relative overflow-hidden">
|
|
||||||
<div className="absolute top-0 right-0 bg-blue-500 text-white text-xs px-3 py-1 transform rotate-12 translate-x-2 -translate-y-1 shadow-sm">
|
|
||||||
Initial
|
|
||||||
</div>
|
|
||||||
<CardContent className="pt-6">
|
|
||||||
<div className="grid grid-cols-3 gap-1 w-24 h-24 border border-outline rounded mx-auto">
|
|
||||||
{initialBoard.map((row, rowIndex) =>
|
|
||||||
row.map((piece, colIndex) => (
|
|
||||||
<div
|
|
||||||
key={`init-${rowIndex}-${colIndex}`}
|
|
||||||
className={`flex items-center justify-center text-xs ${
|
|
||||||
(rowIndex + colIndex) % 2 === 0 ? "bg-surface" : "bg-surface-variant"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{piece === 'white' ? '♘' : piece === 'black' ? '♞' : ''}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="bg-secondary/20 border-secondary">
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<CardTitle className="text-lg">Target Position</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="grid grid-cols-3 gap-1 w-24 h-24 border border-outline rounded mx-auto">
|
|
||||||
{targetBoard.map((row, rowIndex) =>
|
|
||||||
row.map((piece, colIndex) => (
|
|
||||||
<div
|
|
||||||
key={`target-${rowIndex}-${colIndex}`}
|
|
||||||
className={`flex items-center justify-center text-xs ${
|
|
||||||
(rowIndex + colIndex) % 2 === 0 ? "bg-surface" : "bg-surface-variant"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{piece === 'white' ? '♘' : piece === 'black' ? '♞' : ''}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Red: Game rules */}
|
{/* Red: Game rules */}
|
||||||
|
|
@ -314,12 +235,7 @@ const KnightsPuzzle = ({ showSocialShare = false }: KnightsPuzzleProps) => {
|
||||||
<div className="pt-4 border-t border-outline">
|
<div className="pt-4 border-t border-outline">
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Puzzle source:{" "}
|
Puzzle source:{" "}
|
||||||
<a
|
<a href="https://bsky.app/profile/neuwirthe.bsky.social/post/3luhmc7gilc2x" target="_blank" rel="noopener noreferrer" className="text-accent hover:underline inline-flex items-center gap-1">
|
||||||
href="https://bsky.app/profile/neuwirthe.bsky.social/post/3luhmc7gilc2x"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-accent hover:underline inline-flex items-center gap-1"
|
|
||||||
>
|
|
||||||
@neuwirthe.bsky.social
|
@neuwirthe.bsky.social
|
||||||
<ExternalLink className="w-3 h-3" />
|
<ExternalLink className="w-3 h-3" />
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -327,14 +243,7 @@ const KnightsPuzzle = ({ showSocialShare = false }: KnightsPuzzleProps) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Social Share Section */}
|
{/* Social Share Section */}
|
||||||
{showSocialShare && (
|
{showSocialShare && <SocialShare title="Knights Exchange Puzzle" description="Can the black and white knights swap places using legal chess moves? Try this classic puzzle!" />}
|
||||||
<SocialShare
|
</div>;
|
||||||
title="Knights Exchange Puzzle"
|
|
||||||
description="Can the black and white knights swap places using legal chess moves? Try this classic puzzle!"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default KnightsPuzzle;
|
export default KnightsPuzzle;
|
||||||
Loading…
Add table
Add a link
Reference in a new issue