diff --git a/src/App.tsx b/src/App.tsx index 1bac3b1..7702395 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -44,6 +44,8 @@ import Structures from "./pages/theme-pages/discrete-math/Structures"; import Numbers from "./pages/theme-pages/discrete-math/Numbers"; import Graphs from "./pages/theme-pages/discrete-math/Graphs"; import NotFound from "./pages/NotFound"; +import GridTilingPuzzlePage from './pages/GridTilingPuzzlePage'; +import SunnyLinesPuzzlePage from './pages/SunnyLinesPuzzlePage'; const queryClient = new QueryClient(); @@ -77,6 +79,8 @@ const App = () => ( } /> } /> } /> + } /> + } /> {/* Direct interactive routes */} } /> diff --git a/src/components/GridTilingPuzzle.tsx b/src/components/GridTilingPuzzle.tsx new file mode 100644 index 0000000..0b083eb --- /dev/null +++ b/src/components/GridTilingPuzzle.tsx @@ -0,0 +1,416 @@ +import React, { useState, useCallback } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { CheckCircle, XCircle, Info, ChevronDown, ChevronUp } from 'lucide-react'; + +interface Rectangle { + id: number; + startRow: number; + startCol: number; + endRow: number; + endCol: number; +} + +interface GridTilingPuzzleProps { + showSocialShare?: boolean; +} + +const GridTilingPuzzle: React.FC = ({ showSocialShare = true }) => { + const [gridSize, setGridSize] = useState(9); + const [rectangles, setRectangles] = useState([]); + const [isDrawing, setIsDrawing] = useState(false); + const [startCell, setStartCell] = useState<{ row: number; col: number } | null>(null); + const [hoverCell, setHoverCell] = useState<{ row: number; col: number } | null>(null); + const [isValidating, setIsValidating] = useState(false); + const [showInstructions, setShowInstructions] = useState(false); + const [validationResult, setValidationResult] = useState<{ + isValid: boolean; + message: string; + uncoveredCount: number; + } | null>(null); + + const specialSizes = [4, 9, 25]; + + // Different shades of blue for rectangles - starting very dark and getting lighter + const blueShades = [ + 'bg-blue-900', + 'bg-blue-800', + 'bg-blue-700', + 'bg-blue-600', + 'bg-blue-500', + 'bg-blue-400', + 'bg-blue-300', + 'bg-blue-200' + ]; + + const resetPuzzle = () => { + setRectangles([]); + setIsDrawing(false); + setStartCell(null); + setHoverCell(null); + setIsValidating(false); + setValidationResult(null); + }; + + const isCellCovered = (row: number, col: number): boolean => { + return rectangles.some(rect => + row >= rect.startRow && row <= rect.endRow && + col >= rect.startCol && col <= rect.endCol + ); + }; + + const isCellInRectangle = (row: number, col: number, rect: Rectangle): boolean => { + return row >= rect.startRow && row <= rect.endRow && + col >= rect.startCol && col <= rect.endCol; + }; + + const getCellClass = (row: number, col: number): string => { + let classes = 'border border-gray-300 cursor-pointer transition-colors'; + + // Check if cell is covered by any rectangle + const coveredRect = rectangles.find(rect => isCellInRectangle(row, col, rect)); + if (coveredRect) { + const rectIndex = rectangles.findIndex(rect => rect.id === coveredRect.id); + const blueShade = blueShades[rectIndex % blueShades.length]; + classes += ` ${blueShade} hover:${blueShade.replace('bg-', 'bg-').replace('-', '-')}`; + return classes; + } + + // Check if this is the start cell + if (startCell && startCell.row === row && startCell.col === col) { + classes += ' bg-green-500 hover:bg-green-600'; + return classes; + } + + // Check if this would be a valid completion (no overlap) + if (startCell && hoverCell && !isCellCovered(row, col)) { + const minRow = Math.min(startCell.row, hoverCell.row); + const maxRow = Math.max(startCell.row, hoverCell.row); + const minCol = Math.min(startCell.col, hoverCell.col); + const maxCol = Math.max(startCell.col, hoverCell.col); + + if (row >= minRow && row <= maxRow && col >= minCol && col <= maxCol) { + // Check if this preview rectangle would overlap with any existing rectangles + const wouldOverlap = rectangles.some(existingRect => { + return !(maxRow < existingRect.startRow || + minRow > existingRect.endRow || + maxCol < existingRect.startCol || + minCol > existingRect.endCol); + }); + + if (!wouldOverlap) { + classes += ' bg-yellow-300 hover:bg-yellow-400'; + return classes; + } + } + } + + classes += ' bg-white hover:bg-gray-100'; + return classes; + }; + + const handleCellClick = (row: number, col: number) => { + if (isCellCovered(row, col)) return; + + if (!isDrawing) { + // Start drawing a rectangle + setIsDrawing(true); + setStartCell({ row, col }); + } else { + // Complete the rectangle + if (startCell) { + const newRect: Rectangle = { + id: Date.now(), + startRow: Math.min(startCell.row, row), + startCol: Math.min(startCell.col, col), + endRow: Math.max(startCell.row, row), + endCol: Math.max(startCell.col, col) + }; + + // Check if the new rectangle would overlap with any existing rectangles + const wouldOverlap = rectangles.some(existingRect => { + return !(newRect.endRow < existingRect.startRow || + newRect.startRow > existingRect.endRow || + newRect.endCol < existingRect.startCol || + newRect.startCol > existingRect.endCol); + }); + + if (wouldOverlap) { + // Don't place the rectangle if it would overlap + setIsDrawing(false); + setStartCell(null); + setHoverCell(null); + return; + } + + setRectangles(prev => [...prev, newRect]); + setIsDrawing(false); + setStartCell(null); + setHoverCell(null); + } + } + }; + + const handleCellHover = (row: number, col: number) => { + if (isDrawing && startCell) { + setHoverCell({ row, col }); + } + }; + + const handleCellLeave = () => { + setHoverCell(null); + }; + + const validateSolution = () => { + setIsValidating(true); + + // Check if each row and column has exactly one uncovered square + let uncoveredCount = 0; + const rowUncovered = new Array(gridSize).fill(0); + const colUncovered = new Array(gridSize).fill(0); + + for (let row = 0; row < gridSize; row++) { + for (let col = 0; col < gridSize; col++) { + if (!isCellCovered(row, col)) { + uncoveredCount++; + rowUncovered[row]++; + colUncovered[col]++; + } + } + } + + const isValid = rowUncovered.every(count => count === 1) && + colUncovered.every(count => count === 1); + + let message = ''; + if (isValid) { + message = `Perfect! Your solution is valid. You used ${rectangles.length} rectangles.`; + } else { + const invalidRows = rowUncovered.map((count, i) => count !== 1 ? i + 1 : null).filter(Boolean); + const invalidCols = colUncovered.map((count, i) => count !== 1 ? i + 1 : null).filter(Boolean); + message = `Invalid solution. Rows ${invalidRows.join(', ')} and columns ${invalidCols.join(', ')} don't have exactly one uncovered square.`; + } + + setValidationResult({ isValid, message, uncoveredCount }); + setIsValidating(false); + }; + + const removeRectangle = (rectId: number) => { + setRectangles(prev => prev.filter(rect => rect.id !== rectId)); + setValidationResult(null); + }; + + return ( +
+ + + + Grid Tiling Puzzle + +

+ This is P6 on IMO 2025. The given grid size was 2025 in the original problem. +

+
+ +
+

+ Place rectangular tiles on the grid so that each row and each column has exactly one uncovered square. What's the smallest number of rectangles you can use? +

+ +
+
+ +
+ 3 + { + setGridSize(parseInt(e.target.value)); + resetPuzzle(); + }} + className={`w-32 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer ${ + specialSizes.includes(gridSize) ? 'slider-special' : 'slider-normal' + }`} + style={{ + '--thumb-color': specialSizes.includes(gridSize) ? '#10b981' : '#6b7280' + } as React.CSSProperties} + /> + 25 +
+
+ + + + +
+ +
+ + Rectangles: {rectangles.length} + + + Grid: {gridSize}×{gridSize} + {specialSizes.includes(gridSize) && } + +
+
+
+
+ + {/* Instructions */} + + setShowInstructions(!showInstructions)} + > +
+ Instructions + {showInstructions ? ( + + ) : ( + + )} +
+
+ {showInstructions && ( + +
+
    +
  • Click on any empty square to start drawing a rectangle
  • +
  • Click on another square to complete the rectangle
  • +
  • Each row and column must have exactly one uncovered square
  • +
  • Rectangles cannot overlap
  • +
  • Try to use the minimum number of rectangles possible
  • +
+ +
+ + Legend: Green = Start, Yellow = Preview, Blue shades = Placed Rectangles +
+
+
+ )} +
+ + {/* Grid */} + + +
+
+ {Array.from({ length: gridSize * gridSize }, (_, index) => { + const row = Math.floor(index / gridSize); + const col = index % gridSize; + return ( +
handleCellClick(row, col)} + onMouseEnter={() => handleCellHover(row, col)} + onMouseLeave={handleCellLeave} + /> + ); + })} +
+
+ + + + {/* Validation */} +
+ +
+ + {/* Validation Result */} + {validationResult && ( + + +
+ {validationResult.isValid ? ( + + ) : ( + + )} +
+

+ {validationResult.message} +

+

+ Total uncovered squares: {validationResult.uncoveredCount} +

+
+
+
+
+ )} + + {/* Rectangle List */} + {rectangles.length > 0 && ( + + + Placed Rectangles + + +
+ {rectangles.map((rect, index) => ( +
+
+ Rectangle {index + 1}: +
+ + ({rect.startRow + 1},{rect.startCol + 1}) to ({rect.endRow + 1},{rect.endCol + 1}) + +
+ +
+ ))} +
+
+
+ )} +
+ ); +}; + +export default GridTilingPuzzle; \ No newline at end of file diff --git a/src/components/InteractiveIndex.tsx b/src/components/InteractiveIndex.tsx index e514383..4dfc1c5 100644 --- a/src/components/InteractiveIndex.tsx +++ b/src/components/InteractiveIndex.tsx @@ -153,6 +153,24 @@ const allInteractives: Interactive[] = [ path: '/games/nim', theme: 'Games', dateAdded: '2024-07-21' + }, + { + id: 'grid-tiling-puzzle', + title: 'Grid Tiling Puzzle', + description: 'Based on IMO 2025: Place rectangular tiles on a grid so that each row and column has exactly one uncovered square.', + tags: ['puzzle', 'geometry', 'optimization', 'imo', 'contest-problems'], + path: '/contest-problems/grid-tiling', + theme: 'Contest Problems', + dateAdded: '2024-12-22' + }, + { + id: 'sunny-lines-puzzle', + title: 'Sunny Lines Puzzle', + description: 'Based on IMO 2025 P6: Place lines to cover points in a triangle. A line is "sunny" if it\'s not parallel to x-axis, y-axis, or x+y=0.', + tags: ['puzzle', 'geometry', 'lines', 'imo', 'contest-problems'], + path: '/contest-problems/sunny-lines', + theme: 'Contest Problems', + dateAdded: '2024-12-22' } ]; diff --git a/src/components/SunnyLinesPuzzle.tsx b/src/components/SunnyLinesPuzzle.tsx new file mode 100644 index 0000000..bb3ac50 --- /dev/null +++ b/src/components/SunnyLinesPuzzle.tsx @@ -0,0 +1,605 @@ +import React, { useState, useCallback, useRef } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { CheckCircle, XCircle, Info, ChevronDown, ChevronUp, Undo2 } from 'lucide-react'; + +interface Point { + x: number; + y: number; +} + +interface Line { + id: number; + start: Point; + end: Point; + isSunny: boolean; +} + +interface SunnyLinesPuzzleProps { + showSocialShare?: boolean; +} + +const SunnyLinesPuzzle: React.FC = ({ showSocialShare = true }) => { + const [n, setN] = useState(5); + const [lines, setLines] = useState([]); + const [isDrawing, setIsDrawing] = useState(false); + const [startPoint, setStartPoint] = useState(null); + const [hoverPoint, setHoverPoint] = useState(null); + const [showInstructions, setShowInstructions] = useState(false); + const [showResults, setShowResults] = useState(false); + const canvasRef = useRef(null); + + const gridSize = n + 2; + const canvasSize = 600; + const cellSize = canvasSize / gridSize; + + // Check if a line is sunny (not parallel to x-axis, y-axis, or x+y=0) + const isSunnyLine = (start: Point, end: Point): boolean => { + const dx = end.x - start.x; + const dy = end.y - start.y; + + if (dx === 0) return false; // Parallel to y-axis + if (dy === 0) return false; // Parallel to x-axis + if (dx === -dy) return false; // Parallel to x+y=0 (slope = -1) + + return true; + }; + + // Convert canvas coordinates to grid coordinates + const canvasToGrid = (canvasX: number, canvasY: number): Point => { + const x = canvasX / cellSize; + const y = (canvasSize - canvasY) / cellSize; // Flip Y axis + return { x, y }; + }; + + // Convert grid coordinates to canvas coordinates + const gridToCanvas = (gridX: number, gridY: number): Point => { + const x = gridX * cellSize; + const y = canvasSize - (gridY * cellSize); // Flip Y axis + return { x, y }; + }; + + // Snap a point to the nearest grid point or midpoint + const snapToGrid = (point: Point): Point => { + return { + x: Math.round(point.x * 2) / 2, // Snap to 0, 0.5, 1, 1.5, 2, etc. + y: Math.round(point.y * 2) / 2 // Snap to 0, 0.5, 1, 1.5, 2, etc. + }; + }; + + // Extend a line to grid boundaries + const extendLineToBoundaries = (start: Point, end: Point): { start: Point; end: Point } => { + const dx = end.x - start.x; + const dy = end.y - start.y; + + if (dx === 0) { + // Vertical line - extend to top and bottom boundaries + return { + start: { x: start.x, y: 0 }, + end: { x: end.x, y: gridSize } + }; + } + + if (dy === 0) { + // Horizontal line - extend to left and right boundaries + return { + start: { x: 0, y: start.y }, + end: { x: gridSize, y: end.y } + }; + } + + // Diagonal line - extend to grid boundaries + const slope = dy / dx; + + // Find intersection with left boundary (x = 0) + const leftY = start.y - slope * start.x; + const leftPoint = { x: 0, y: leftY }; + + // Find intersection with right boundary (x = gridSize) + const rightY = start.y + slope * (gridSize - start.x); + const rightPoint = { x: gridSize, y: rightY }; + + // Find intersection with bottom boundary (y = 0) + const bottomX = start.x - start.y / slope; + const bottomPoint = { x: bottomX, y: 0 }; + + // Find intersection with top boundary (y = gridSize) + const topX = start.x + (gridSize - start.y) / slope; + const topPoint = { x: topX, y: gridSize }; + + // Find the two boundary points that are actually on the grid + const boundaryPoints = []; + + if (leftY >= 0 && leftY <= gridSize) boundaryPoints.push(leftPoint); + if (rightY >= 0 && rightY <= gridSize) boundaryPoints.push(rightPoint); + if (bottomX >= 0 && bottomX <= gridSize) boundaryPoints.push(bottomPoint); + if (topX >= 0 && topX <= gridSize) boundaryPoints.push(topPoint); + + // Return the two points that are furthest apart + if (boundaryPoints.length >= 2) { + let maxDistance = 0; + let bestPair = { start: boundaryPoints[0], end: boundaryPoints[1] }; + + for (let i = 0; i < boundaryPoints.length; i++) { + for (let j = i + 1; j < boundaryPoints.length; j++) { + const dist = Math.sqrt( + Math.pow(boundaryPoints[i].x - boundaryPoints[j].x, 2) + + Math.pow(boundaryPoints[i].y - boundaryPoints[j].y, 2) + ); + if (dist > maxDistance) { + maxDistance = dist; + bestPair = { start: boundaryPoints[i], end: boundaryPoints[j] }; + } + } + } + + return bestPair; + } + + // Fallback to original points if boundary calculation fails + return { start, end }; + }; + + // Check if a point (a,b) is in the triangle a+b <= n+1 + const isInTriangle = (a: number, b: number): boolean => { + return a >= 1 && b >= 1 && a + b <= n + 1; + }; + + // Check if a point is covered by any line + const isPointCovered = (a: number, b: number): boolean => { + return lines.some(line => { + const dx = line.end.x - line.start.x; + const dy = line.end.y - line.start.y; + + // Check if point (a,b) lies on the line + const t1 = (a - line.start.x) / dx; + const t2 = (b - line.start.y) / dy; + + // If line is vertical or horizontal, check differently + if (dx === 0) { + return Math.abs(a - line.start.x) < 0.01; + } + if (dy === 0) { + return Math.abs(b - line.start.y) < 0.01; + } + + return Math.abs(t1 - t2) < 0.01 && t1 >= 0 && t1 <= 1; + }); + }; + + const handleCanvasClick = (event: React.MouseEvent) => { + if (!canvasRef.current) return; + + const rect = canvasRef.current.getBoundingClientRect(); + const canvasX = event.clientX - rect.left; + const canvasY = event.clientY - rect.top; + const gridPoint = snapToGrid(canvasToGrid(canvasX, canvasY)); + + if (!isDrawing) { + setStartPoint(gridPoint); + setIsDrawing(true); + } else { + if (startPoint) { + const extendedLine = extendLineToBoundaries(startPoint, gridPoint); + const newLine: Line = { + id: Date.now(), + start: extendedLine.start, + end: extendedLine.end, + isSunny: isSunnyLine(extendedLine.start, extendedLine.end) + }; + + setLines(prev => [...prev, newLine]); + setIsDrawing(false); + setStartPoint(null); + setHoverPoint(null); + } + } + }; + + const handleCanvasMouseMove = (event: React.MouseEvent) => { + if (!canvasRef.current || !isDrawing) return; + + const rect = canvasRef.current.getBoundingClientRect(); + const canvasX = event.clientX - rect.left; + const canvasY = event.clientY - rect.top; + const gridPoint = snapToGrid(canvasToGrid(canvasX, canvasY)); + + setHoverPoint(gridPoint); + }; + + const handleCanvasMouseLeave = () => { + setHoverPoint(null); + }; + + const resetPuzzle = () => { + setLines([]); + setIsDrawing(false); + setStartPoint(null); + setHoverPoint(null); + setShowResults(false); + }; + + const undoLastLine = () => { + setLines(prev => prev.slice(0, -1)); + setShowResults(false); + }; + + const calculateResults = () => { + setShowResults(true); + }; + + const getLineColor = (line: Line): string => { + return line.isSunny ? '#10b981' : '#ef4444'; + }; + + const renderGrid = () => { + const cells = []; + + // Render grid cells for reference - include both integer points and midpoints + for (let x = 0; x <= gridSize * 2; x++) { + for (let y = 0; y <= gridSize * 2; y++) { + const gridX = x / 2; + const gridY = y / 2; + const canvasPoint = gridToCanvas(gridX, gridY); + + // Make integer points slightly larger and midpoints smaller + const isIntegerPoint = x % 2 === 0 && y % 2 === 0; + const size = isIntegerPoint ? 2 : 1; + const opacity = isIntegerPoint ? 0.8 : 0.4; + + cells.push( +
+ ); + } + } + + return cells; + }; + + const renderTriangle = () => { + const trianglePoints = []; + + // Render blue dots at integer lattice points where a + b <= n + 1 + for (let a = 1; a <= n; a++) { + for (let b = 1; b <= n; b++) { + if (a + b <= n + 1) { + const canvasPoint = gridToCanvas(a, b); + trianglePoints.push( + + ); + } + } + } + + return trianglePoints; + }; + + const renderCoveredPoints = () => { + const coveredPoints = []; + + // Check only integer lattice points in the triangle for coverage + for (let a = 1; a <= n; a++) { + for (let b = 1; b <= n; b++) { + if (a + b <= n + 1 && isPointCovered(a, b)) { + const canvasPoint = gridToCanvas(a, b); + coveredPoints.push( + + ); + } + } + } + + return coveredPoints; + }; + + const renderLines = () => { + return lines.map(line => { + const startCanvas = gridToCanvas(line.start.x, line.start.y); + const endCanvas = gridToCanvas(line.end.x, line.end.y); + + return ( + + ); + }); + }; + + const renderPreviewLine = () => { + if (!isDrawing || !startPoint || !hoverPoint) return null; + + const extendedLine = extendLineToBoundaries(startPoint, hoverPoint); + const startCanvas = gridToCanvas(extendedLine.start.x, extendedLine.start.y); + const endCanvas = gridToCanvas(extendedLine.end.x, extendedLine.end.y); + + const isSunny = isSunnyLine(extendedLine.start, extendedLine.end); + + return ( + + ); + }; + + const countSunnyLines = () => lines.filter(line => line.isSunny).length; + const countNonSunnyLines = () => lines.filter(line => !line.isSunny).length; + + const allTrianglePointsCovered = () => { + // Check only integer lattice points where a + b <= n + 1 + for (let a = 1; a <= n; a++) { + for (let b = 1; b <= n; b++) { + if (a + b <= n + 1 && !isPointCovered(a, b)) { + return false; + } + } + } + return true; + }; + + return ( +
+ + + + Sunny Lines Puzzle + +

+ Based on IMO 2025 Problem P6 +

+
+ +
+

+ Place lines to cover all blue dots in the triangle where a + b ≤ n + 1. + A line is "sunny" if it's not parallel to the x-axis, y-axis, or x + y = 0. +

+ +
+
+ +
+ 3 + { + setN(parseInt(e.target.value)); + resetPuzzle(); + }} + className="w-32 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" + /> + 10 +
+
+ + + + +
+ +
+ + Lines: {lines.length} + + + Sunny: {countSunnyLines()} + + + Non-sunny: {countNonSunnyLines()} + +
+
+
+
+ + {/* Instructions */} + + setShowInstructions(!showInstructions)} + > +
+ Instructions + {showInstructions ? ( + + ) : ( + + )} +
+
+ {showInstructions && ( + +
+
    +
  • Click anywhere to start drawing a line
  • +
  • Click again to complete the line
  • +
  • Cover all blue dots in the triangle (a + b ≤ n + 1)
  • +
  • Green lines are "sunny" (not parallel to x-axis, y-axis, or x + y = 0)
  • +
  • Red dashed lines are non-sunny
  • +
  • Try to minimize the number of lines used
  • +
+ +
+ + Legend: Blue dots = Points to cover, Green dots = Covered points, Green lines = Sunny, Red dashed = Non-sunny +
+
+
+ )} +
+ + {/* Canvas */} + + +
+
+ + {renderLines()} + {renderPreviewLine()} + {renderTriangle()} + {renderCoveredPoints()} + + {renderGrid()} + + {/* Coordinate labels */} +
+ (0,0) +
+
+ ({gridSize},{gridSize}) +
+
+
+
+
+ + {/* Calculate Results */} +
+ +
+ + {/* Results */} + {showResults && ( + + +
+
+ +
+

+ Results for n = {n} +

+

+ k = {countSunnyLines()} (number of sunny lines) +

+
+
+ +
+
+

Coverage Status:

+

+ {allTrianglePointsCovered() ? + "✅ All triangle points are covered" : + "❌ Some triangle points are not covered" + } +

+
+ +
+

Line Summary:

+

+ Total: {lines.length} | Sunny: {countSunnyLines()} | Non-sunny: {countNonSunnyLines()} +

+
+
+
+
+
+ )} + + {/* Line List */} + {lines.length > 0 && ( + + + Placed Lines + + +
+ {lines.map((line, index) => ( +
+
+ Line {index + 1}: +
+ + ({line.start.x.toFixed(1)}, {line.start.y.toFixed(1)}) to ({line.end.x.toFixed(1)}, {line.end.y.toFixed(1)}) + +
+ + {line.isSunny ? '☀️ Sunny' : '🌧️ Non-sunny'} + +
+
+ ))} +
+
+
+ )} +
+ ); +}; + +export default SunnyLinesPuzzle; \ No newline at end of file diff --git a/src/index.css b/src/index.css index b592fcc..161fadf 100644 --- a/src/index.css +++ b/src/index.css @@ -131,4 +131,39 @@ All colors MUST be HSL. h1, h2, h3, h4, h5, h6 { @apply font-heading; } +} + +/* Custom slider styling */ +.slider-special::-webkit-slider-thumb, +.slider-normal::-webkit-slider-thumb { + appearance: none; + height: 20px; + width: 20px; + border-radius: 50%; + cursor: pointer; +} + +.slider-special::-webkit-slider-thumb { + background: #10b981; +} + +.slider-normal::-webkit-slider-thumb { + background: #6b7280; +} + +.slider-special::-moz-range-thumb, +.slider-normal::-moz-range-thumb { + height: 20px; + width: 20px; + border-radius: 50%; + cursor: pointer; + border: none; +} + +.slider-special::-moz-range-thumb { + background: #10b981; +} + +.slider-normal::-moz-range-thumb { + background: #6b7280; } \ No newline at end of file diff --git a/src/pages/GridTilingPuzzlePage.tsx b/src/pages/GridTilingPuzzlePage.tsx new file mode 100644 index 0000000..c76f51c --- /dev/null +++ b/src/pages/GridTilingPuzzlePage.tsx @@ -0,0 +1,11 @@ +import GridTilingPuzzle from '@/components/GridTilingPuzzle'; + +const GridTilingPuzzlePage = () => { + return ( +
+ +
+ ); +}; + +export default GridTilingPuzzlePage; \ No newline at end of file diff --git a/src/pages/SunnyLinesPuzzlePage.tsx b/src/pages/SunnyLinesPuzzlePage.tsx new file mode 100644 index 0000000..e81fd52 --- /dev/null +++ b/src/pages/SunnyLinesPuzzlePage.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import SunnyLinesPuzzle from '@/components/SunnyLinesPuzzle'; + +const SunnyLinesPuzzlePage: React.FC = () => { + return ( +
+ +
+ ); +}; + +export default SunnyLinesPuzzlePage; \ No newline at end of file diff --git a/src/pages/theme-pages/ContestProblems.tsx b/src/pages/theme-pages/ContestProblems.tsx index d3b9d7f..2e5d2fa 100644 --- a/src/pages/theme-pages/ContestProblems.tsx +++ b/src/pages/theme-pages/ContestProblems.tsx @@ -1,15 +1,119 @@ -import ComingSoon from "@/components/ComingSoon"; +import { useState } from "react"; +import { Link } from "react-router-dom"; +import { Search } from "lucide-react"; import Layout from "@/components/Layout"; +import InteractiveCard from "@/components/InteractiveCard"; +import GridTilingPuzzle from "@/components/GridTilingPuzzle"; +import SunnyLinesPuzzle from "@/components/SunnyLinesPuzzle"; + +interface Interactive { + id: string; + title: string; + description: string; + tags: string[]; + component: React.ComponentType; + greenScreenPath: string; +} + +const interactives: Interactive[] = [ + { + id: "grid-tiling-puzzle", + title: "Grid Tiling Puzzle", + description: "Based on IMO 2025: Place rectangular tiles on a grid so that each row and column has exactly one uncovered square.", + tags: ["puzzle", "geometry", "optimization", "imo", "contest-problems"], + component: GridTilingPuzzle, + greenScreenPath: "/contest-problems/grid-tiling" + }, + { + id: "sunny-lines-puzzle", + title: "Sunny Lines Puzzle", + description: "Based on IMO 2025 P6: Place lines to cover points in a triangle. A line is 'sunny' if it's not parallel to x-axis, y-axis, or x+y=0.", + tags: ["puzzle", "geometry", "lines", "imo", "contest-problems"], + component: SunnyLinesPuzzle, + greenScreenPath: "/contest-problems/sunny-lines" + } +]; const ContestProblems = () => { + const [searchTerm, setSearchTerm] = useState(""); + const [selectedInteractive, setSelectedInteractive] = useState(null); + + const filteredInteractives = interactives.filter(interactive => + interactive.title.toLowerCase().includes(searchTerm.toLowerCase()) || + interactive.description.toLowerCase().includes(searchTerm.toLowerCase()) || + interactive.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase())) + ); + + if (selectedInteractive) { + const InteractiveComponent = selectedInteractive.component; + return ( +
+
+
+ + + + Back to Themes → + +
+ +
+
+ ); + } + return ( -
-
- +
+
+
+ + ← Back to Themes + +

Contest Problems

+
+ +
+

+ Practice with problems from programming competitions, mathematical olympiads, and algorithmic challenges with interactive solutions and explanations. +

+
+ +
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-outline rounded-md bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" + /> +
+ +
+ {filteredInteractives.map(interactive => ( + setSelectedInteractive(interactive)} + /> + ))} +
+ + {filteredInteractives.length === 0 && ( +
+

+ No interactives found matching "{searchTerm}" +

+
+ )}