From ca5d1e89b8caf5d0904f93a64b5b405c56c8f76e Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 16:00:13 +0000 Subject: [PATCH 1/7] Changes --- src/components/LadybugClockPuzzle.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/LadybugClockPuzzle.tsx b/src/components/LadybugClockPuzzle.tsx index 4c6de70..e61e078 100644 --- a/src/components/LadybugClockPuzzle.tsx +++ b/src/components/LadybugClockPuzzle.tsx @@ -670,8 +670,7 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare {' '}and demonstrated in{' '} this YouTube short - - . It illustrates a beautiful result about random walks on cycle graphs: every position (except the start) has equal probability of being visited last. + .

From 8894b039263c7edbe10047510e3b6628ff9a2835 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 16:02:29 +0000 Subject: [PATCH 2/7] Visual edit in Lovable Edited UI in Lovable --- src/components/LadybugClockPuzzle.tsx | 335 ++++++++------------------ 1 file changed, 97 insertions(+), 238 deletions(-) diff --git a/src/components/LadybugClockPuzzle.tsx b/src/components/LadybugClockPuzzle.tsx index e61e078..1fab2ca 100644 --- a/src/components/LadybugClockPuzzle.tsx +++ b/src/components/LadybugClockPuzzle.tsx @@ -8,11 +8,9 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Play, Pause, RotateCcw, SkipForward, FastForward, Info, ExternalLink, BarChart3 } from 'lucide-react'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, Cell } from 'recharts'; import SocialShare from '@/components/SocialShare'; - interface LadybugClockPuzzleProps { showSocialShare?: boolean; } - interface SimulationState { position: number; // 0-11 (0 = 12 o'clock, 1 = 1 o'clock, etc.) visited: Set; @@ -21,10 +19,10 @@ interface SimulationState { isComplete: boolean; moveCount: number; } - const CLOCK_NUMBERS = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; - -const LadybugClockPuzzle: React.FC = ({ showSocialShare = true }) => { +const LadybugClockPuzzle: React.FC = ({ + showSocialShare = true +}) => { // Simulation state const [simulation, setSimulation] = useState({ position: 0, @@ -34,33 +32,32 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare isComplete: false, moveCount: 0 }); - + // Controls const [isPlaying, setIsPlaying] = useState(false); const [speed, setSpeed] = useState(50); // 1-100, maps to delay const [numPositions, setNumPositions] = useState(12); - + // Statistics const [lastPaintedCounts, setLastPaintedCounts] = useState>({}); const [totalSimulations, setTotalSimulations] = useState(0); const [averageMoves, setAverageMoves] = useState(0); const [totalMoves, setTotalMoves] = useState(0); - + // User prediction const [userPrediction, setUserPrediction] = useState(null); const [showPredictionResult, setShowPredictionResult] = useState(false); - + // Refs const animationRef = useRef(null); const isPlayingRef = useRef(isPlaying); - useEffect(() => { isPlayingRef.current = isPlaying; }, [isPlaying]); // Get delay from speed (inverse relationship) const getDelay = useCallback(() => { - return Math.max(50, 1000 - (speed * 9)); + return Math.max(50, 1000 - speed * 9); }, [speed]); // Reset simulation @@ -83,18 +80,15 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare const step = useCallback(() => { setSimulation(prev => { if (prev.isComplete) return prev; - + // Random move: clockwise or counterclockwise const direction = Math.random() < 0.5 ? 1 : -1; const newPosition = ((prev.position + direction) % numPositions + numPositions) % numPositions; - const newVisited = new Set(prev.visited); const wasNewlyPainted = !newVisited.has(newPosition); newVisited.add(newPosition); - const isNowComplete = newVisited.size === numPositions; const lastPainted = isNowComplete && wasNewlyPainted ? newPosition : prev.lastPainted; - return { position: newPosition, visited: newVisited, @@ -111,13 +105,11 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare if (!isPlaying || simulation.isComplete) { return; } - const timeoutId = setTimeout(() => { if (isPlayingRef.current && !simulation.isComplete) { step(); } }, getDelay()); - return () => clearTimeout(timeoutId); }, [isPlaying, simulation, step, getDelay]); @@ -134,44 +126,40 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare const visited = new Set([0]); let moveCount = 0; let lastPainted = 0; - while (visited.size < numPositions) { const direction = Math.random() < 0.5 ? 1 : -1; position = ((position + direction) % numPositions + numPositions) % numPositions; - if (!visited.has(position)) { lastPainted = position; visited.add(position); } moveCount++; } - - return { lastPainted, moveCount }; + return { + lastPainted, + moveCount + }; }, [numPositions]); // Run to completion const runToCompletion = useCallback(() => { setIsPlaying(false); - let currentSim = simulation; let position = currentSim.position; const visited = new Set(currentSim.visited); let moveCount = currentSim.moveCount; let lastPainted = currentSim.lastPainted; const path = [...currentSim.path]; - while (visited.size < numPositions) { const direction = Math.random() < 0.5 ? 1 : -1; position = ((position + direction) % numPositions + numPositions) % numPositions; path.push(position); - if (!visited.has(position)) { lastPainted = position; visited.add(position); } moveCount++; } - setSimulation({ position, visited, @@ -184,15 +172,15 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare // Run batch simulations const runBatchSimulations = useCallback((count: number) => { - const newCounts = { ...lastPaintedCounts }; + const newCounts = { + ...lastPaintedCounts + }; let batchMoves = 0; - for (let i = 0; i < count; i++) { const result = runInstantSimulation(); newCounts[result.lastPainted] = (newCounts[result.lastPainted] || 0) + 1; batchMoves += result.moveCount; } - setLastPaintedCounts(newCounts); setTotalSimulations(prev => prev + count); setTotalMoves(prev => prev + batchMoves); @@ -225,7 +213,7 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare // Calculate clock position for a given index const getClockPosition = (index: number, radius: number) => { - const angle = (index / numPositions) * 2 * Math.PI - Math.PI / 2; + const angle = index / numPositions * 2 * Math.PI - Math.PI / 2; return { x: Math.cos(angle) * radius, y: Math.sin(angle) * radius @@ -233,10 +221,12 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare }; // Prepare chart data - const chartData = Array.from({ length: numPositions - 1 }, (_, i) => { + const chartData = Array.from({ + length: numPositions - 1 + }, (_, i) => { const pos = i + 1; // 1 to n-1 (0/12 can't be last) const count = lastPaintedCounts[pos] || 0; - const percentage = totalSimulations > 0 ? (count / totalSimulations) * 100 : 0; + const percentage = totalSimulations > 0 ? count / totalSimulations * 100 : 0; return { position: positionToNumber(pos), count, @@ -244,15 +234,9 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare label: `${positionToNumber(pos)}` }; }); - const theoreticalProbability = 100 / (numPositions - 1); - - const shareUrl = typeof window !== 'undefined' - ? `${window.location.origin}/puzzles/ladybug-clock` - : ''; - - return ( -
+ const shareUrl = typeof window !== 'undefined' ? `${window.location.origin}/puzzles/ladybug-clock` : ''; + return
{/* Header */}

The Ladybug Clock Puzzle

@@ -272,71 +256,34 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare {/* Hour markers and numbers */} - {Array.from({ length: numPositions }, (_, i) => { - const pos = getClockPosition(i, 100); - const isPainted = simulation.visited.has(i); - const isLast = simulation.isComplete && simulation.lastPainted === i; - const isCurrent = simulation.position === i; - - return ( - + {Array.from({ + length: numPositions + }, (_, i) => { + const pos = getClockPosition(i, 100); + const isPainted = simulation.visited.has(i); + const isLast = simulation.isComplete && simulation.lastPainted === i; + const isCurrent = simulation.position === i; + return {/* Tick mark */} - + {/* Number circle background */} - + {/* Glow effect for newly painted or last */} - {(isPainted || isLast) && ( - - )} + {(isPainted || isLast) && } {/* Number text */} - + {positionToNumber(i)} - - ); - })} + ; + })} {/* Ladybug */} {(() => { - const ladybugPos = getClockPosition(simulation.position, 100); - return ( - + const ladybugPos = getClockPosition(simulation.position, 100); + return {/* Ladybug body */} {/* Head */} @@ -351,9 +298,8 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare {/* Antennae */} - - ); - })()} + ; + })()} {/* Center decoration */} @@ -374,13 +320,11 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare
- {simulation.isComplete && simulation.lastPainted !== null && ( -
+ {simulation.isComplete && simulation.lastPainted !== null &&

🎉 Complete! Last painted: {positionToNumber(simulation.lastPainted)}

-
- )} +
}
@@ -394,32 +338,17 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare
- - - @@ -432,26 +361,12 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare
- setSpeed(v[0])} - min={1} - max={100} - step={1} - className="w-full" - /> + setSpeed(v[0])} min={1} max={100} step={1} className="w-full" />
- setNumPositions(v[0])} - min={4} - max={24} - step={2} - className="w-full" - /> + setNumPositions(v[0])} min={4} max={24} step={2} className="w-full" />
@@ -466,16 +381,9 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare
- {[100, 1000, 10000].map(count => ( - - ))} + )} @@ -485,18 +393,15 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare

Total simulations: {totalSimulations.toLocaleString()}

- {totalSimulations > 0 && ( -

+ {totalSimulations > 0 &&

Average moves to complete: {averageMoves.toFixed(1)} -

- )} +

}
{/* User Prediction */} - {totalSimulations === 0 && ( - + {totalSimulations === 0 && Make a Prediction! 🤔 @@ -505,32 +410,22 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare Which number do you think is most likely to be painted last?

- {Array.from({ length: numPositions - 1 }, (_, i) => i + 1).map(pos => ( - - ))} + )}
- {userPrediction !== null && ( -

+ {userPrediction !== null &&

You predicted: {positionToNumber(userPrediction)}. Run some simulations to see! -

- )} +

} -
- )} +
}
{/* Statistics Chart */} - {totalSimulations > 0 && ( - + {totalSimulations > 0 && Statistics: Which number was painted last? @@ -540,52 +435,42 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare
- + - `${v.toFixed(1)}%`} - domain={[0, Math.max(theoreticalProbability * 1.5, 15)]} - /> - [`${value.toFixed(2)}%`, 'Probability']} - /> - + `${v.toFixed(1)}%`} domain={[0, Math.max(theoreticalProbability * 1.5, 15)]} /> + [`${value.toFixed(2)}%`, 'Probability']} /> + - {chartData.map((entry, index) => ( - - ))} + {chartData.map((entry, index) => )}
- {userPrediction !== null && ( -
+ {userPrediction !== null &&

Your prediction ({positionToNumber(userPrediction)}):{' '} {((lastPaintedCounts[userPrediction] || 0) / totalSimulations * 100).toFixed(2)}% {' '}— Theory predicts all numbers have equal probability of{' '} {theoreticalProbability.toFixed(2)}%!

-
- )} +
}

@@ -594,34 +479,21 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare

-
- )} +
} {/* Path History */} - {simulation.path.length > 1 && ( - + {simulation.path.length > 1 && Path History
- {simulation.path.map((pos, i) => ( - + {simulation.path.map((pos, i) => {positionToNumber(pos)} - - ))} + )}
-
- )} +
} {/* Educational Info */} @@ -641,19 +513,14 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare
  • The simulation ends when all numbers have been visited at least once
  • -

    The Surprising Result

    -

    - Intuitively, you might expect the number 6 (opposite to 12) to be the most likely to be painted last, - since it's the farthest from the starting point. However, the probability is exactly 1/{numPositions - 1} for - every number from 1 to {numPositions - 1}! This is because once all but one number is painted, the random walk - will eventually reach that last number, regardless of where it is. -

    -

    Mathematical Connections

    + + +
      -
    • Random Walks on Cycles: This is an example of a random walk on a cycle graph
    • -
    • Cover Time: The number of moves to visit all vertices is called the "cover time"
    • -
    • Gambler's Ruin: Related to the classic probability problem of a gambler with a finite amount
    • + + +
    @@ -675,15 +542,7 @@ const LadybugClockPuzzle: React.FC = ({ showSocialShare
    - {showSocialShare && ( - - )} - - ); + {showSocialShare && } + ; }; - -export default LadybugClockPuzzle; +export default LadybugClockPuzzle; \ No newline at end of file From cb5a243cf36413bb79c776727791dfc0cdb1b10a Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 16:03:12 +0000 Subject: [PATCH 3/7] Changes --- src/components/LadybugClockPuzzle.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/LadybugClockPuzzle.tsx b/src/components/LadybugClockPuzzle.tsx index 1fab2ca..4ad2044 100644 --- a/src/components/LadybugClockPuzzle.tsx +++ b/src/components/LadybugClockPuzzle.tsx @@ -409,10 +409,10 @@ const LadybugClockPuzzle: React.FC = ({

    Which number do you think is most likely to be painted last?

    -
    +
    {Array.from({ length: numPositions - 1 - }, (_, i) => i + 1).map(pos => )}
    From 828f2dba667696a55389a83f161277d2203bfd3b Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 03:04:28 +0000 Subject: [PATCH 4/7] Changes --- src/App.tsx | 2 + src/components/ErdosDiscrepancyPuzzle.tsx | 902 ++++++++++++++++++++++ src/components/InteractiveIndex.tsx | 9 + src/pages/ErdosDiscrepancyPuzzlePage.tsx | 16 + src/pages/theme-pages/Puzzles.tsx | 10 + 5 files changed, 939 insertions(+) create mode 100644 src/components/ErdosDiscrepancyPuzzle.tsx create mode 100644 src/pages/ErdosDiscrepancyPuzzlePage.tsx diff --git a/src/App.tsx b/src/App.tsx index 55ab15d..9c68f71 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -67,6 +67,7 @@ import BagchalGamePage from './pages/BagchalGamePage'; import AscDescGridPuzzlePage from './pages/AscDescGridPuzzlePage'; import EternalDominationGamePage from './pages/EternalDominationGamePage'; import LadybugClockPuzzlePage from './pages/LadybugClockPuzzlePage'; +import ErdosDiscrepancyPuzzlePage from './pages/ErdosDiscrepancyPuzzlePage'; const queryClient = new QueryClient(); @@ -144,6 +145,7 @@ const App = () => ( } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/components/ErdosDiscrepancyPuzzle.tsx b/src/components/ErdosDiscrepancyPuzzle.tsx new file mode 100644 index 0000000..8046d40 --- /dev/null +++ b/src/components/ErdosDiscrepancyPuzzle.tsx @@ -0,0 +1,902 @@ +import React, { useState, useEffect, useCallback, useMemo } 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, AlertTitle } from '@/components/ui/alert'; +import { Slider } from '@/components/ui/slider'; +import { + Play, RotateCcw, Info, ChevronDown, ChevronUp, + ArrowLeft, ArrowRight, Skull, AlertTriangle, + BookOpen, Zap, Eye, Grid3X3, TrendingUp, Pause, + Volume2, VolumeX, Target, Trophy +} from 'lucide-react'; +import SocialShare from '@/components/SocialShare'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; + +interface ErdosDiscrepancyPuzzleProps { + showSocialShare?: boolean; +} + +type GamePhase = 'intro' | 'writing' | 'captor-reveal' | 'walking' | 'fallen' | 'survived'; +type Direction = 1 | -1 | null; + +interface DiscrepancyResult { + d: number; + k: number; + sum: number; + positions: number[]; +} + +const ErdosDiscrepancyPuzzle: React.FC = ({ showSocialShare = false }) => { + // Core game state + const [sequence, setSequence] = useState([]); + const [discrepancyBound, setDiscrepancyBound] = useState(1); + const [gamePhase, setGamePhase] = useState('intro'); + const [showRules, setShowRules] = useState(false); + + // Walk visualization state + const [selectedD, setSelectedD] = useState(1); + const [walkPosition, setWalkPosition] = useState(0); + const [walkStep, setWalkStep] = useState(0); + const [isWalking, setIsWalking] = useState(false); + const [walkSpeed, setWalkSpeed] = useState(500); + + // Prisoner's Walk story mode + const [captorChosenD, setCaptorChosenD] = useState(null); + const [prisonerPosition, setPrisonerPosition] = useState(0); + const [prisonerStep, setPrisonerStep] = useState(0); + + // Heatmap state + const [hoveredCell, setHoveredCell] = useState<{d: number, k: number} | null>(null); + const [highlightedSubsequence, setHighlightedSubsequence] = useState([]); + + // Best score tracking + const [bestLength, setBestLength] = useState(null); + + // Load best score from localStorage + useEffect(() => { + const saved = localStorage.getItem(`erdos-best-length-c${discrepancyBound}`); + if (saved) { + setBestLength(parseInt(saved)); + } + }, [discrepancyBound]); + + // Calculate all discrepancies for current sequence + const allDiscrepancies = useMemo((): DiscrepancyResult[] => { + if (sequence.length === 0) return []; + + const results: DiscrepancyResult[] = []; + const n = sequence.length; + + for (let d = 1; d <= n; d++) { + let sum = 0; + const positions: number[] = []; + + for (let k = 1; k * d <= n; k++) { + const pos = k * d - 1; // 0-indexed + if (sequence[pos] !== null) { + sum += sequence[pos]!; + positions.push(k * d); + results.push({ + d, + k, + sum, + positions: [...positions] + }); + } + } + } + + return results; + }, [sequence]); + + // Find maximum discrepancy + const maxDiscrepancy = useMemo(() => { + if (allDiscrepancies.length === 0) return { value: 0, result: null }; + + let maxAbs = 0; + let maxResult: DiscrepancyResult | null = null; + + for (const result of allDiscrepancies) { + if (Math.abs(result.sum) > maxAbs) { + maxAbs = Math.abs(result.sum); + maxResult = result; + } + } + + return { value: maxAbs, result: maxResult }; + }, [allDiscrepancies]); + + // Check if current sequence violates bound + const hasViolation = maxDiscrepancy.value > discrepancyBound; + + // Find worst step size for captor + const findWorstD = useCallback((): number => { + let worstD = 1; + let worstDiscrepancy = 0; + + for (const result of allDiscrepancies) { + if (Math.abs(result.sum) > worstDiscrepancy) { + worstDiscrepancy = Math.abs(result.sum); + worstD = result.d; + } + } + + return worstD; + }, [allDiscrepancies]); + + // Get discrepancy for specific d,k + const getDiscrepancy = (d: number, k: number): number | null => { + const result = allDiscrepancies.find(r => r.d === d && r.k === k); + return result ? result.sum : null; + }; + + // Toggle sequence value + const togglePosition = (index: number) => { + if (gamePhase !== 'writing') return; + + setSequence(prev => { + const newSeq = [...prev]; + if (newSeq[index] === null) { + newSeq[index] = 1; + } else if (newSeq[index] === 1) { + newSeq[index] = -1; + } else { + newSeq[index] = 1; + } + return newSeq; + }); + }; + + // Add new position to sequence + const addPosition = () => { + if (gamePhase !== 'writing') return; + setSequence(prev => [...prev, null]); + }; + + // Start the game + const startGame = () => { + setSequence([null, null, null, null, null]); + setGamePhase('writing'); + setWalkPosition(0); + setWalkStep(0); + setPrisonerPosition(0); + setPrisonerStep(0); + setCaptorChosenD(null); + setHighlightedSubsequence([]); + }; + + // Reset the game + const resetGame = () => { + setSequence([]); + setGamePhase('intro'); + setWalkPosition(0); + setWalkStep(0); + setIsWalking(false); + setPrisonerPosition(0); + setPrisonerStep(0); + setCaptorChosenD(null); + setHighlightedSubsequence([]); + }; + + // Handle captor reveal (Prisoner's Walk mode) + const revealCaptor = () => { + const worstD = findWorstD(); + setCaptorChosenD(worstD); + setGamePhase('captor-reveal'); + setPrisonerPosition(0); + setPrisonerStep(0); + }; + + // Start prisoner walking + const startPrisonerWalk = () => { + if (captorChosenD === null) return; + setGamePhase('walking'); + setPrisonerPosition(0); + setPrisonerStep(0); + }; + + // Prisoner walk animation + useEffect(() => { + if (gamePhase !== 'walking' || captorChosenD === null) return; + + const interval = setInterval(() => { + setPrisonerStep(prev => { + const nextStep = prev + 1; + const posIndex = nextStep * captorChosenD - 1; + + if (posIndex >= sequence.length || sequence[posIndex] === null) { + // Sequence ended without falling + setGamePhase('survived'); + clearInterval(interval); + + // Save best score + const validLength = sequence.filter(s => s !== null).length; + if (!bestLength || validLength > bestLength) { + setBestLength(validLength); + localStorage.setItem(`erdos-best-length-c${discrepancyBound}`, validLength.toString()); + } + + return prev; + } + + const newPosition = prisonerPosition + sequence[posIndex]!; + setPrisonerPosition(newPosition); + + if (Math.abs(newPosition) > discrepancyBound) { + setGamePhase('fallen'); + clearInterval(interval); + return nextStep; + } + + return nextStep; + }); + }, walkSpeed); + + return () => clearInterval(interval); + }, [gamePhase, captorChosenD, sequence, prisonerPosition, discrepancyBound, walkSpeed, bestLength]); + + // Manual walk control + const walkSubsequence = () => { + if (isWalking) { + setIsWalking(false); + return; + } + + setWalkPosition(0); + setWalkStep(0); + setIsWalking(true); + }; + + // Walk animation effect + useEffect(() => { + if (!isWalking) return; + + const interval = setInterval(() => { + setWalkStep(prev => { + const nextStep = prev + 1; + const posIndex = nextStep * selectedD - 1; + + if (posIndex >= sequence.length || sequence[posIndex] === null) { + setIsWalking(false); + return prev; + } + + setWalkPosition(current => current + sequence[posIndex]!); + return nextStep; + }); + }, walkSpeed); + + return () => clearInterval(interval); + }, [isWalking, selectedD, sequence, walkSpeed]); + + // Get color for heatmap cell + const getHeatmapColor = (discrepancy: number | null): string => { + if (discrepancy === null) return 'bg-muted/30'; + + const absVal = Math.abs(discrepancy); + if (absVal > discrepancyBound) { + return 'bg-destructive/80 text-destructive-foreground'; + } + if (absVal === discrepancyBound) { + return 'bg-amber-500/70 text-white'; + } + if (absVal === 0) { + return 'bg-emerald-500/50 text-emerald-900 dark:text-emerald-100'; + } + + const intensity = absVal / discrepancyBound; + if (intensity < 0.5) { + return 'bg-emerald-400/40 text-emerald-900 dark:text-emerald-100'; + } + return 'bg-amber-400/50 text-amber-900 dark:text-amber-100'; + }; + + // Max valid k for given d + const maxK = (d: number) => Math.floor(sequence.length / d); + + // Highlighted positions for selected d + const selectedPositions = useMemo(() => { + const positions: number[] = []; + for (let k = 1; k * selectedD <= sequence.length; k++) { + positions.push(k * selectedD); + } + return positions; + }, [selectedD, sequence.length]); + + // Get fill status of sequence + const filledCount = sequence.filter(s => s !== null).length; + const allFilled = filledCount === sequence.length && sequence.length > 0; + + // Known optimal sequences + const optimalSequences: Record = { + 1: [1, 1, -1, 1, -1, -1, -1, 1, 1, 1, -1], // One of the optimal 11-length sequences for C=1 + }; + + const loadOptimalSequence = () => { + const optimal = optimalSequences[discrepancyBound]; + if (optimal) { + setSequence([...optimal]); + setGamePhase('writing'); + } + }; + + return ( +
    + {/* Header */} +
    +
    +

    + The Erdős Discrepancy Problem +

    +

    + Can you write instructions to survive the tunnel forever? +

    +
    +
    + +
    +
    + + {/* Rules Panel */} + {showRules && ( + + +
    +
    +

    + The Prisoner's Dilemma +

    +

    + You're trapped in a tunnel with a deadly cliff {discrepancyBound} step{discrepancyBound > 1 ? 's' : ''} to your LEFT and + a pit of vipers {discrepancyBound} step{discrepancyBound > 1 ? 's' : ''} to your RIGHT. +

    +

    + Write a list of instructions: each is either LEFT or RIGHT. +

    +

    + The evil captor then picks a step size d and you follow every d-th instruction. +

    +
    +
    +

    + The Impossible Task +

    +

    + C = 1: Maximum possible sequence length is 11 +

    +

    + C = 2: Maximum possible sequence length is 1,160 +

    +

    + Terence Tao proved in 2015: No infinite sequence can survive! +

    +
    +
    +
    +
    + )} + + {/* Game Phase: Intro */} + {gamePhase === 'intro' && ( + + +
    + +

    The Prisoner's Walk

    +

    + You wake up in a dark tunnel. There's a cliff to your left and vipers to your right. + Your captor demands you write a sequence of LEFT/RIGHT instructions... + but they get to choose which instructions you follow. +

    + +
    + +
    + 1 + setDiscrepancyBound(v[0])} + min={1} + max={3} + step={1} + className="flex-1" + /> + 3 +
    +

    + C=1: Max 11 steps | C=2: Max 1,160 steps | C=3: Unknown (very large) +

    +
    + +
    + + {discrepancyBound === 1 && ( + + )} +
    + + {bestLength && ( +

    + Your best: {bestLength} steps with C={discrepancyBound} +

    + )} +
    +
    +
    + )} + + {/* Main Game UI */} + {gamePhase !== 'intro' && ( +
    + {/* Status Bar */} +
    + + Length: {sequence.length} + + + Filled: {filledCount}/{sequence.length} + + + Max Discrepancy: {maxDiscrepancy.value} + {maxDiscrepancy.result && ` (d=${maxDiscrepancy.result.d}, k=${maxDiscrepancy.result.k})`} + + + Bound: C = {discrepancyBound} + + {bestLength && ( + + + Best: {bestLength} + + )} +
    + + {/* Violation Alert */} + {hasViolation && gamePhase === 'writing' && ( + + + Bound Exceeded! + + The subsequence with d={maxDiscrepancy.result?.d}, k={maxDiscrepancy.result?.k} has + discrepancy {maxDiscrepancy.value}, which exceeds your bound of {discrepancyBound}. + + + )} + + + + + + Sequence + + + + Walk + + + + Heatmap + + + + {/* Sequence Builder Tab */} + + + + + Write Your Instructions + {gamePhase === 'writing' && ( + Click to toggle + )} + + + + {/* Sequence Grid */} +
    +
    + {sequence.map((val, idx) => { + const pos = idx + 1; + const isHighlighted = highlightedSubsequence.includes(pos) || selectedPositions.includes(pos); + const isViolationPart = maxDiscrepancy.result?.positions.includes(pos) && hasViolation; + + return ( + + ); + })} + + {gamePhase === 'writing' && ( + + )} +
    +
    + + {/* Controls */} +
    + {gamePhase === 'writing' && ( + <> + + + + )} +
    +
    +
    +
    + + {/* Walk Visualization Tab */} + + + + Walk Visualization + + + {/* Step size selector */} +
    +
    + +
    + {Array.from({ length: Math.min(sequence.length, 8) }, (_, i) => i + 1).map(d => ( + + ))} +
    +
    + +
    + + setWalkSpeed(1000 - v[0])} + min={0} + max={900} + step={100} + className="w-24" + /> +
    +
    + + {/* Number line visualization */} +
    + {/* Danger zones */} +
    + CLIFF +
    +
    + VIPERS +
    + + {/* Safe zone markers */} +
    + {Array.from({ length: discrepancyBound * 2 + 1 }, (_, i) => i - discrepancyBound).map(pos => ( +
    + {pos} +
    + ))} +
    + + {/* Walker position */} +
    +
    + 👤 +
    +
    +
    + + {/* Walk info */} +
    + Position: {walkPosition} + Steps taken: {walkStep} + Following: positions {selectedPositions.slice(0, 5).join(', ')}{selectedPositions.length > 5 ? '...' : ''} +
    + + {/* Walk controls */} +
    + + +
    +
    +
    +
    + + {/* Heatmap Tab */} + + {/* This is actually handled in the walk tab above */} + + + + + + Discrepancy Heatmap + + +

    + Each cell shows the sum of values at positions d, 2d, 3d, ..., kd. + Red cells exceed the bound. +

    + +
    + + + + + {Array.from({ length: Math.min(12, sequence.length) }, (_, i) => i + 1).map(k => ( + + ))} + + + + {Array.from({ length: Math.min(8, sequence.length) }, (_, i) => i + 1).map(d => ( + + + {Array.from({ length: Math.min(12, sequence.length) }, (_, i) => i + 1).map(k => { + const discrepancy = getDiscrepancy(d, k); + const isValid = d * k <= sequence.length; + + return ( + + ); + })} + + ))} + +
    d \ k{k}
    {d} { + if (isValid) { + setHoveredCell({ d, k }); + const result = allDiscrepancies.find(r => r.d === d && r.k === k); + if (result) { + setHighlightedSubsequence(result.positions); + } + } + }} + onMouseLeave={() => { + setHoveredCell(null); + setHighlightedSubsequence([]); + }} + > + {isValid && discrepancy !== null ? discrepancy : ''} +
    +
    + + {hoveredCell && ( +
    + d={hoveredCell.d}, k={hoveredCell.k}: Looking at positions{' '} + {Array.from({ length: hoveredCell.k }, (_, i) => (i + 1) * hoveredCell.d).join(', ')} +
    + )} +
    +
    +
    +
    + + {/* Captor Reveal Phase */} + {gamePhase === 'captor-reveal' && captorChosenD !== null && ( + + + +

    The Captor Speaks...

    +

    + "Interesting sequence... Let me see... I choose d = {captorChosenD}!" +

    +

    + You will follow every {captorChosenD === 1 ? '' : captorChosenD === 2 ? '2nd' : captorChosenD === 3 ? '3rd' : `${captorChosenD}th`} instruction: + positions {Array.from({ length: Math.floor(sequence.length / captorChosenD) }, (_, i) => (i + 1) * captorChosenD).slice(0, 8).join(', ')}... +

    + +
    +
    + )} + + {/* Walking Phase */} + {gamePhase === 'walking' && captorChosenD !== null && ( + + +
    +

    Walking with d = {captorChosenD}...

    +

    + Step {prisonerStep} | Position: {prisonerPosition} +

    +
    + + {/* Tunnel visualization */} +
    +
    + 🏔️ +
    +
    + 🐍 +
    + +
    + 🚶 +
    +
    +
    +
    + )} + + {/* Fallen Phase */} + {gamePhase === 'fallen' && ( + + + You Fell! + + After {prisonerStep} steps with d={captorChosenD}, you ended up at position {prisonerPosition}. + The captor found your weakness! Your sequence lasted {sequence.length} instructions. +
    + + +
    +
    +
    + )} + + {/* Survived Phase (temporary - sequence ended) */} + {gamePhase === 'survived' && ( + + + Sequence Complete! + + Your {sequence.length}-instruction sequence survived d={captorChosenD}! + But remember: no sequence can survive forever. Try making it longer! +
    + + +
    +
    +
    + )} +
    + )} + + {/* Acknowledgement */} + + +

    Credits & History

    +

    + This puzzle is based on the Erdős Discrepancy Problem, posed by Paul Erdős in the 1930s + (with a $500 prize!). The "Prisoner's Walk" framing comes from various mathematical expositions, + popularized by James Grime's singingbanana video. +

    +

    + 2014: Boris Konev & Alexei Lisitsa proved C=2 has maximum length 1,160 using SAT solvers + (generating a 13GB proof!). 2015: Terence Tao proved that NO infinite sequence + can have bounded discrepancy, settling the problem completely. +

    +
    +
    + + {/* Social Share */} + {showSocialShare !== false && ( + + )} +
    + ); +}; + +export default ErdosDiscrepancyPuzzle; diff --git a/src/components/InteractiveIndex.tsx b/src/components/InteractiveIndex.tsx index a9ce43c..d2dbe67 100644 --- a/src/components/InteractiveIndex.tsx +++ b/src/components/InteractiveIndex.tsx @@ -288,6 +288,15 @@ const allInteractives: Interactive[] = [ path: '/puzzles/ladybug-clock', theme: 'Puzzles', dateAdded: '2025-01-21' + }, + { + id: 'erdos-discrepancy', + title: 'The Erdős Discrepancy Problem', + description: 'Can you write instructions to survive the tunnel forever? Explore this famous mathematical puzzle through the Prisoner\'s Walk game.', + tags: ['erdos', 'discrepancy', 'sequences', 'game-theory', 'impossibility', 'puzzle', 'number-theory'], + path: '/puzzles/erdos-discrepancy', + theme: 'Puzzles', + dateAdded: '2025-01-22' } ]; diff --git a/src/pages/ErdosDiscrepancyPuzzlePage.tsx b/src/pages/ErdosDiscrepancyPuzzlePage.tsx new file mode 100644 index 0000000..cb323f8 --- /dev/null +++ b/src/pages/ErdosDiscrepancyPuzzlePage.tsx @@ -0,0 +1,16 @@ +import Layout from "@/components/Layout"; +import ErdosDiscrepancyPuzzle from "@/components/ErdosDiscrepancyPuzzle"; + +const ErdosDiscrepancyPuzzlePage = () => { + return ( + +
    +
    + +
    +
    +
    + ); +}; + +export default ErdosDiscrepancyPuzzlePage; diff --git a/src/pages/theme-pages/Puzzles.tsx b/src/pages/theme-pages/Puzzles.tsx index 323270e..5e6b9b1 100644 --- a/src/pages/theme-pages/Puzzles.tsx +++ b/src/pages/theme-pages/Puzzles.tsx @@ -115,6 +115,16 @@ const Puzzles = () => { difficulty: "Intermediate" as const, duration: "5-15 min", participants: "1 player" + }, + { + id: "erdos-discrepancy", + title: "The Erdős Discrepancy Problem", + description: "Can you write instructions to survive the prisoner's tunnel? Explore this famous mathematical puzzle and discover why no sequence can keep you safe forever!", + path: "/puzzles/erdos-discrepancy", + tags: ["Number Theory", "Sequences", "Impossibility", "Game Theory"], + difficulty: "Advanced" as const, + duration: "10-30 min", + participants: "1 player" } ]; From 8f2a086df69409ef8fde158399d69884b89a0cef Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:07:30 +0000 Subject: [PATCH 5/7] Standardize page wrappers, add showSocialShare prop, and add Cmd+K search - Standardize all page wrappers to use consistent min-h-screen bg-background pattern - Remove Layout wrapper usage from pages that had it - Remove page-level SocialShare and headers, move into components - Add showSocialShare prop to all interactive components for consistent control - Add Cmd+K / Ctrl+K command palette search across all interactives - Export allInteractives from InteractiveIndex for reuse Co-Authored-By: Neeldhara Misra --- src/App.tsx | 2 + src/components/AssistedNimGame.tsx | 20 +++-- src/components/CommandSearch.tsx | 65 +++++++++++++++ src/components/EternalDominationGame.tsx | 14 +++- src/components/FerrersRogersRamanujan.tsx | 14 +++- src/components/GoldCoinGame.tsx | 19 +++-- src/components/InteractiveIndex.tsx | 4 +- src/components/NimGame.tsx | 14 ++-- src/components/RentDivisionPuzzle.tsx | 10 ++- src/components/RulesOfInferencePlayground.tsx | 12 ++- src/pages/AscDescGridPuzzlePage.tsx | 9 +- src/pages/AssistedNimGamePage.tsx | 9 +- src/pages/BagchalGamePage.tsx | 5 +- src/pages/BalancedTernaryGamePage.tsx | 8 +- src/pages/BinaryNumberGamePage.tsx | 8 +- src/pages/BulgarianSolitairePage.tsx | 30 ++----- src/pages/BurnsidesLemmaPage.tsx | 5 +- src/pages/CubeColoringPage.tsx | 10 +-- src/pages/DominoRetilingPuzzlePage.tsx | 27 ++---- src/pages/ErdosDiscrepancyPuzzlePage.tsx | 11 +-- src/pages/EternalDominationGamePage.tsx | 83 +------------------ src/pages/FerrersRogersRamanujanPage.tsx | 23 +---- src/pages/GoldCoinGamePage.tsx | 9 +- src/pages/LadybugClockPuzzlePage.tsx | 25 +----- src/pages/NimGamePage.tsx | 9 +- src/pages/ParityBitsGamePage.tsx | 11 +-- src/pages/PebblePlacementGamePage.tsx | 30 ++----- src/pages/PresentsPuzzlePage.tsx | 7 +- src/pages/RentDivisionPuzzlePage.tsx | 30 +------ src/pages/RulesOfInferencePlaygroundPage.tsx | 29 +------ src/pages/StackingBlocksPage.tsx | 30 ++----- src/pages/SubtractionGamePage.tsx | 10 ++- src/pages/TernaryNumberGamePage.tsx | 8 +- src/pages/ZeckendorfGamePage.tsx | 8 +- src/pages/ZeckendorfSearchTrickPage.tsx | 8 +- 35 files changed, 259 insertions(+), 357 deletions(-) create mode 100644 src/components/CommandSearch.tsx diff --git a/src/App.tsx b/src/App.tsx index 9c68f71..f94e757 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import { Toaster as Sonner } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import ErrorBoundary from "@/components/ErrorBoundary"; +import CommandSearch from "@/components/CommandSearch"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import Index from "./pages/Index"; import InteractiveIndex from "./components/InteractiveIndex"; @@ -78,6 +79,7 @@ const App = () => ( + } /> } /> diff --git a/src/components/AssistedNimGame.tsx b/src/components/AssistedNimGame.tsx index ee3fa28..d04028c 100644 --- a/src/components/AssistedNimGame.tsx +++ b/src/components/AssistedNimGame.tsx @@ -66,7 +66,11 @@ const getOddPowers = (heaps: number[]): Set => { return oddPowers; }; -const AssistedNimGame: React.FC = () => { +interface AssistedNimGameProps { + showSocialShare?: boolean; +} + +const AssistedNimGame: React.FC = ({ showSocialShare = true }) => { const [mode, setMode] = useState(null); const [showSetup, setShowSetup] = useState(false); const [userHeapCount, setUserHeapCount] = useState(3); @@ -384,14 +388,14 @@ const AssistedNimGame: React.FC = () => { - {/* Social Share */} - + {showSocialShare && ( + + )}
    ); }; -export default AssistedNimGame; \ No newline at end of file +export default AssistedNimGame; \ No newline at end of file diff --git a/src/components/CommandSearch.tsx b/src/components/CommandSearch.tsx new file mode 100644 index 0000000..1b5a6fb --- /dev/null +++ b/src/components/CommandSearch.tsx @@ -0,0 +1,65 @@ +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { allInteractives } from "@/components/InteractiveIndex"; +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { Badge } from "@/components/ui/badge"; + +const CommandSearch = () => { + const [open, setOpen] = useState(false); + const navigate = useNavigate(); + + useEffect(() => { + const down = (e: KeyboardEvent) => { + if (e.key === "k" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + setOpen((prev) => !prev); + } + }; + document.addEventListener("keydown", down); + return () => document.removeEventListener("keydown", down); + }, []); + + const handleSelect = (path: string) => { + setOpen(false); + navigate(path); + }; + + return ( + + + + No interactives found. + + {allInteractives.map((interactive) => ( + handleSelect(interactive.path)} + > +
    +
    + {interactive.title} + + {interactive.theme} + +
    + + {interactive.description} + +
    +
    + ))} +
    +
    +
    + ); +}; + +export default CommandSearch; diff --git a/src/components/EternalDominationGame.tsx b/src/components/EternalDominationGame.tsx index 4720bf1..fd2e461 100644 --- a/src/components/EternalDominationGame.tsx +++ b/src/components/EternalDominationGame.tsx @@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Shield, Target, RotateCcw, Info } from "lucide-react"; +import SocialShare from '@/components/SocialShare'; // Extended good rectangle: 10 columns × 12 rows (following Fig. 10) // Border: columns 0 and 9, rows 0/1 (top) and 10/11 (bottom) @@ -349,7 +350,11 @@ const defendAttack = ( }; -const EternalDominationGame: React.FC = () => { +interface EternalDominationGameProps { + showSocialShare?: boolean; +} + +const EternalDominationGame: React.FC = ({ showSocialShare = true }) => { const [guards, setGuards] = useState>(generateInitialGuards); const [attackHistory, setAttackHistory] = useState([]); const [message, setMessage] = useState("Click any unguarded cell to attack."); @@ -679,6 +684,13 @@ const EternalDominationGame: React.FC = () => { )} + + {showSocialShare && ( + + )} ); }; diff --git a/src/components/FerrersRogersRamanujan.tsx b/src/components/FerrersRogersRamanujan.tsx index 1da4500..a5c95ac 100644 --- a/src/components/FerrersRogersRamanujan.tsx +++ b/src/components/FerrersRogersRamanujan.tsx @@ -4,8 +4,13 @@ import { Button } from "@/components/ui/button"; import { Slider } from "@/components/ui/slider"; import { Input } from "@/components/ui/input"; import { toast } from "sonner"; +import SocialShare from '@/components/SocialShare'; -const FerrersRogersRamanujan = () => { +interface FerrersRogersRamanujanProps { + showSocialShare?: boolean; +} + +const FerrersRogersRamanujan = ({ showSocialShare = true }: FerrersRogersRamanujanProps) => { const [n, setN] = useState(14); const [partitionSliders, setPartitionSliders] = useState([]); const [committedPartition, setCommittedPartition] = useState([]); @@ -330,6 +335,13 @@ const FerrersRogersRamanujan = () => { The hook sizes form a new partition where parts differ by at least 2.

    + + {showSocialShare && ( + + )} ); diff --git a/src/components/GoldCoinGame.tsx b/src/components/GoldCoinGame.tsx index f2dd211..456dc17 100644 --- a/src/components/GoldCoinGame.tsx +++ b/src/components/GoldCoinGame.tsx @@ -18,7 +18,11 @@ interface GameState { moveHistory: string[]; } -const GoldCoinGame: React.FC = () => { +interface GoldCoinGameProps { + showSocialShare?: boolean; +} + +const GoldCoinGame: React.FC = ({ showSocialShare = true }) => { const [gameState, setGameState] = useState({ track: Array(15).fill('empty'), currentPlayer: 0, @@ -421,13 +425,14 @@ const GoldCoinGame: React.FC = () => { - + {showSocialShare && ( + + )} ); }; -export default GoldCoinGame; \ No newline at end of file +export default GoldCoinGame; \ No newline at end of file diff --git a/src/components/InteractiveIndex.tsx b/src/components/InteractiveIndex.tsx index d2dbe67..3017d22 100644 --- a/src/components/InteractiveIndex.tsx +++ b/src/components/InteractiveIndex.tsx @@ -18,7 +18,7 @@ interface Interactive { } // This would ideally be generated from the actual components, but for now we'll hardcode -const allInteractives: Interactive[] = [ +export const allInteractives: Interactive[] = [ { id: 'binary-number-game', title: 'Binary Number Representation', @@ -549,4 +549,4 @@ const InteractiveIndex = () => { ); }; -export default InteractiveIndex; \ No newline at end of file +export default InteractiveIndex; \ No newline at end of file diff --git a/src/components/NimGame.tsx b/src/components/NimGame.tsx index 924a523..5fe070c 100644 --- a/src/components/NimGame.tsx +++ b/src/components/NimGame.tsx @@ -27,7 +27,11 @@ const heapColors = [ '#f87171', '#a78bfa', '#fbbf24', '#60a5fa', '#34d399', '#f472b6', '#f87171', '#a78bfa', '#facc15', '#38bdf8', ]; -const NimGame: React.FC = () => { +interface NimGameProps { + showSocialShare?: boolean; +} + +const NimGame: React.FC = ({ showSocialShare = true }) => { const [mode, setMode] = useState(null); const [showSetup, setShowSetup] = useState(false); const [userHeapCount, setUserHeapCount] = useState(3); @@ -275,16 +279,14 @@ const NimGame: React.FC = () => { - {/* Social Share */} -
    + {showSocialShare && ( -
    + )} ); }; -export default NimGame; \ No newline at end of file +export default NimGame; \ No newline at end of file diff --git a/src/components/RentDivisionPuzzle.tsx b/src/components/RentDivisionPuzzle.tsx index b06a5b1..51f9951 100644 --- a/src/components/RentDivisionPuzzle.tsx +++ b/src/components/RentDivisionPuzzle.tsx @@ -11,6 +11,7 @@ import { SelectValue, } from "@/components/ui/select"; import { RefreshCw, User, UserRound, Users, UserCheck } from "lucide-react"; +import SocialShare from '@/components/SocialShare'; const AGENTS = ["Alice", "Bob", "Charlie", "Dana"]; const AGENT_ICONS = [User, UserRound, Users, UserCheck]; @@ -23,9 +24,10 @@ const ROOM_COLORS = [ interface RentDivisionPuzzleProps { onComplete?: (completed: boolean) => void; + showSocialShare?: boolean; } -const RentDivisionPuzzle = ({ onComplete }: RentDivisionPuzzleProps) => { +const RentDivisionPuzzle = ({ onComplete, showSocialShare = true }: RentDivisionPuzzleProps) => { // V-matrix: valuations[agent][room] const [valuations, setValuations] = useState(() => Array.from({ length: 4 }, () => @@ -469,6 +471,12 @@ const RentDivisionPuzzle = ({ onComplete }: RentDivisionPuzzleProps) => {

    + {showSocialShare && ( + + )} ); }; diff --git a/src/components/RulesOfInferencePlayground.tsx b/src/components/RulesOfInferencePlayground.tsx index 1f226f8..54d85b9 100644 --- a/src/components/RulesOfInferencePlayground.tsx +++ b/src/components/RulesOfInferencePlayground.tsx @@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { useToast } from "@/hooks/use-toast"; import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; +import SocialShare from '@/components/SocialShare'; // Rules of Inference Playground (Example 1: Resolution) // Layout: 3 columns (Premises | Notepad lines with rule margin | Rules) @@ -196,7 +197,7 @@ function DropZone({ children, onDrop, className }: { children?: React.ReactNode; ); } -export default function RulesOfInferencePlayground({ onComplete }: { onComplete?: (completed: boolean) => void }) { +export default function RulesOfInferencePlayground({ onComplete, showSocialShare = true }: { onComplete?: (completed: boolean) => void; showSocialShare?: boolean }) { const { toast } = useToast(); const [exampleId, setExampleId] = useState(EXAMPLES[0].id); const example = useMemo(() => EXAMPLES.find((e) => e.id === exampleId)!, [exampleId]); @@ -579,6 +580,13 @@ export default function RulesOfInferencePlayground({ onComplete }: { onComplete? + + {showSocialShare && ( + + )} ); -} \ No newline at end of file +} diff --git a/src/pages/AscDescGridPuzzlePage.tsx b/src/pages/AscDescGridPuzzlePage.tsx index 7963768..da7dde4 100644 --- a/src/pages/AscDescGridPuzzlePage.tsx +++ b/src/pages/AscDescGridPuzzlePage.tsx @@ -1,13 +1,12 @@ -import Layout from "@/components/Layout"; import AscDescGridPuzzle from "@/components/AscDescGridPuzzle"; const AscDescGridPuzzlePage = () => { return ( - -
    - +
    +
    +
    - +
    ); }; diff --git a/src/pages/AssistedNimGamePage.tsx b/src/pages/AssistedNimGamePage.tsx index 233302a..61b9527 100644 --- a/src/pages/AssistedNimGamePage.tsx +++ b/src/pages/AssistedNimGamePage.tsx @@ -1,12 +1,13 @@ -import React from 'react'; import AssistedNimGame from '@/components/AssistedNimGame'; const AssistedNimGamePage = () => { return ( -
    - +
    +
    + +
    ); }; -export default AssistedNimGamePage; \ No newline at end of file +export default AssistedNimGamePage; \ No newline at end of file diff --git a/src/pages/BagchalGamePage.tsx b/src/pages/BagchalGamePage.tsx index 2e1ff48..3d65664 100644 --- a/src/pages/BagchalGamePage.tsx +++ b/src/pages/BagchalGamePage.tsx @@ -1,13 +1,12 @@ -import Layout from "@/components/Layout"; import BagchalGame from "@/components/BagchalGame"; const BagchalGamePage = () => { return ( - +
    - +
    ); }; diff --git a/src/pages/BalancedTernaryGamePage.tsx b/src/pages/BalancedTernaryGamePage.tsx index 61f4057..bd70437 100644 --- a/src/pages/BalancedTernaryGamePage.tsx +++ b/src/pages/BalancedTernaryGamePage.tsx @@ -2,12 +2,12 @@ import BalancedTernaryGame from '@/components/BalancedTernaryGame'; const BalancedTernaryGamePage = () => { return ( -
    -
    - +
    +
    +
    ); }; -export default BalancedTernaryGamePage; \ No newline at end of file +export default BalancedTernaryGamePage; diff --git a/src/pages/BinaryNumberGamePage.tsx b/src/pages/BinaryNumberGamePage.tsx index d12b6c1..15b8a5c 100644 --- a/src/pages/BinaryNumberGamePage.tsx +++ b/src/pages/BinaryNumberGamePage.tsx @@ -2,12 +2,12 @@ import BinaryNumberGame from '@/components/BinaryNumberGame'; const BinaryNumberGamePage = () => { return ( -
    -
    - +
    +
    +
    ); }; -export default BinaryNumberGamePage; \ No newline at end of file +export default BinaryNumberGamePage; diff --git a/src/pages/BulgarianSolitairePage.tsx b/src/pages/BulgarianSolitairePage.tsx index 2a9dfc6..0f6651d 100644 --- a/src/pages/BulgarianSolitairePage.tsx +++ b/src/pages/BulgarianSolitairePage.tsx @@ -1,29 +1,13 @@ import BulgarianSolitaire from "@/components/BulgarianSolitaire"; -import { useSearchParams } from "react-router-dom"; const BulgarianSolitairePage = () => { - const [searchParams] = useSearchParams(); - const from = searchParams.get('from'); - - if (from === 'puzzles') { - return ( -
    -
    -
    - -
    - -
    + return ( +
    +
    +
    - ); - } - - return ; +
    + ); }; -export default BulgarianSolitairePage; \ No newline at end of file +export default BulgarianSolitairePage; diff --git a/src/pages/BurnsidesLemmaPage.tsx b/src/pages/BurnsidesLemmaPage.tsx index c3b7303..713c547 100644 --- a/src/pages/BurnsidesLemmaPage.tsx +++ b/src/pages/BurnsidesLemmaPage.tsx @@ -1,13 +1,12 @@ -import Layout from '@/components/Layout'; import BurnsidesLemma from '@/components/BurnsidesLemma'; const BurnsidesLemmaPage = () => { return ( - +
    - +
    ); }; diff --git a/src/pages/CubeColoringPage.tsx b/src/pages/CubeColoringPage.tsx index 30ddb00..1d8d941 100644 --- a/src/pages/CubeColoringPage.tsx +++ b/src/pages/CubeColoringPage.tsx @@ -1,16 +1,12 @@ -import Layout from '@/components/Layout'; import CubeColoring from '@/components/CubeColoring'; const CubeColoringPage = () => { return ( - +
    - +
    - +
    ); }; diff --git a/src/pages/DominoRetilingPuzzlePage.tsx b/src/pages/DominoRetilingPuzzlePage.tsx index e3f7af7..8170ba1 100644 --- a/src/pages/DominoRetilingPuzzlePage.tsx +++ b/src/pages/DominoRetilingPuzzlePage.tsx @@ -1,28 +1,13 @@ -import Layout from '@/components/Layout'; import DominoRetilingPuzzle from '@/components/DominoRetilingPuzzle'; -import { useLocation, Link } from 'react-router-dom'; -import { ArrowLeft } from 'lucide-react'; const DominoRetilingPuzzlePage = () => { - const location = useLocation(); - const fromPuzzles = location.search.includes('from=puzzles'); - return ( - - {fromPuzzles && ( -
    - - - Back to Puzzles - -
    - )} - -
    +
    +
    + +
    +
    ); }; -export default DominoRetilingPuzzlePage; \ No newline at end of file +export default DominoRetilingPuzzlePage; diff --git a/src/pages/ErdosDiscrepancyPuzzlePage.tsx b/src/pages/ErdosDiscrepancyPuzzlePage.tsx index cb323f8..2578ef0 100644 --- a/src/pages/ErdosDiscrepancyPuzzlePage.tsx +++ b/src/pages/ErdosDiscrepancyPuzzlePage.tsx @@ -1,15 +1,12 @@ -import Layout from "@/components/Layout"; import ErdosDiscrepancyPuzzle from "@/components/ErdosDiscrepancyPuzzle"; const ErdosDiscrepancyPuzzlePage = () => { return ( - -
    -
    - -
    +
    +
    +
    - +
    ); }; diff --git a/src/pages/EternalDominationGamePage.tsx b/src/pages/EternalDominationGamePage.tsx index bd81bb9..54506fe 100644 --- a/src/pages/EternalDominationGamePage.tsx +++ b/src/pages/EternalDominationGamePage.tsx @@ -1,87 +1,12 @@ -import Layout from "@/components/Layout"; import EternalDominationGame from "@/components/EternalDominationGame"; -import SocialShare from "@/components/SocialShare"; -import { Badge } from "@/components/ui/badge"; -import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; const EternalDominationGamePage = () => { return ( - -
    -
    -
    - Miscellany -

    Eternal Domination on a Grid

    -

    - Explore the m-eternal domination problem on a 12×10 grid. Guards must maintain - a dominating set while defending against arbitrary attack sequences. -

    -
    - - - - - - Mathematical Background - -

    - Dominating Set: A subset S of vertices in a graph G such that - every vertex not in S has a neighbor in S. Think of guards placed at certain - vertices that can "watch" their neighbors. -

    -

    - m-Eternal Domination: A two-player game where the defender - places guards on vertices, and the attacker repeatedly attacks unguarded vertices. - The defender responds by moving guards (all can move simultaneously, but only to - adjacent vertices) such that one guard ends on the attacked vertex and the guards - still form a dominating set. -

    -

    - The Challenge: The defender wins if they can maintain a dominating - set forever against any attack sequence. The m-eternal domination number γ∞(G) is - the minimum number of guards needed to win. -

    -
    -
    - - - Defense Strategy - -

    - This implementation uses the strategy from research on finite grids: -

    -
      -
    • Border guards: All vertices on the border (rows 0, 1, 8, 9 and - columns 0, 11) are always guarded, forming a protective cycle C.
    • -
    • Interior guards: Placed in a pattern that ensures every interior - vertex is dominated by exactly one guard.
    • -
    • Defense mechanism: When attacked, interior guards shift toward - the attack, potentially pushing a guard to the border. Border guards shift along - "complementary paths" to fill the resulting gaps.
    • -
    -
    -
    - - - Reference - -

    - Based on: "m-Eternal Domination and Variants on Some Classes of Finite and - Infinite Graphs" by Calamoneri et al. (CIAC 2025), which establishes bounds - and strategies for eternal domination on various grid types including square, - hexagonal, and triangular grids. -

    -
    -
    -
    - - -
    +
    +
    +
    - +
    ); }; diff --git a/src/pages/FerrersRogersRamanujanPage.tsx b/src/pages/FerrersRogersRamanujanPage.tsx index c343f8e..35253d1 100644 --- a/src/pages/FerrersRogersRamanujanPage.tsx +++ b/src/pages/FerrersRogersRamanujanPage.tsx @@ -1,27 +1,12 @@ -import Layout from "@/components/Layout"; import FerrersRogersRamanujan from "@/components/FerrersRogersRamanujan"; -import SocialShare from "@/components/SocialShare"; const FerrersRogersRamanujanPage = () => { return ( - -
    -
    -

    Ferrers Diagram & Rogers-Ramanujan Partition

    -

    - Explore integer partitions through visual Ferrers diagrams and discover the elegant Rogers-Ramanujan transformation. -

    -
    - - - - +
    +
    +
    - +
    ); }; diff --git a/src/pages/GoldCoinGamePage.tsx b/src/pages/GoldCoinGamePage.tsx index d838ae9..0ef4b78 100644 --- a/src/pages/GoldCoinGamePage.tsx +++ b/src/pages/GoldCoinGamePage.tsx @@ -1,12 +1,13 @@ -import React from 'react'; import GoldCoinGame from '@/components/GoldCoinGame'; const GoldCoinGamePage = () => { return ( -
    - +
    +
    + +
    ); }; -export default GoldCoinGamePage; \ No newline at end of file +export default GoldCoinGamePage; \ No newline at end of file diff --git a/src/pages/LadybugClockPuzzlePage.tsx b/src/pages/LadybugClockPuzzlePage.tsx index df6554a..71bdf85 100644 --- a/src/pages/LadybugClockPuzzlePage.tsx +++ b/src/pages/LadybugClockPuzzlePage.tsx @@ -1,29 +1,12 @@ -import Layout from '@/components/Layout'; import LadybugClockPuzzle from '@/components/LadybugClockPuzzle'; -import { useLocation, Link } from 'react-router-dom'; -import { ArrowLeft } from 'lucide-react'; const LadybugClockPuzzlePage = () => { - const location = useLocation(); - const fromPuzzles = location.search.includes('from=puzzles'); - return ( - - {fromPuzzles && ( -
    - - - Back to Puzzles - -
    - )} -
    - +
    +
    +
    - +
    ); }; diff --git a/src/pages/NimGamePage.tsx b/src/pages/NimGamePage.tsx index aa202f6..f34463a 100644 --- a/src/pages/NimGamePage.tsx +++ b/src/pages/NimGamePage.tsx @@ -1,12 +1,13 @@ -import React from 'react'; import NimGame from '@/components/NimGame'; const NimGamePage = () => { return ( -
    - +
    +
    + +
    ); }; -export default NimGamePage; \ No newline at end of file +export default NimGamePage; \ No newline at end of file diff --git a/src/pages/ParityBitsGamePage.tsx b/src/pages/ParityBitsGamePage.tsx index 6ba5a0b..58949d2 100644 --- a/src/pages/ParityBitsGamePage.tsx +++ b/src/pages/ParityBitsGamePage.tsx @@ -1,12 +1,13 @@ -import Layout from "@/components/Layout"; import ParityBitsGame from "@/components/ParityBitsGame"; const ParityBitsGamePage = () => { return ( - - - +
    +
    + +
    +
    ); }; -export default ParityBitsGamePage; \ No newline at end of file +export default ParityBitsGamePage; diff --git a/src/pages/PebblePlacementGamePage.tsx b/src/pages/PebblePlacementGamePage.tsx index e6013d1..c359304 100644 --- a/src/pages/PebblePlacementGamePage.tsx +++ b/src/pages/PebblePlacementGamePage.tsx @@ -1,29 +1,13 @@ import PebblePlacementGame from "@/components/PebblePlacementGame"; -import { useSearchParams } from "react-router-dom"; const PebblePlacementGamePage = () => { - const [searchParams] = useSearchParams(); - const from = searchParams.get('from'); - - if (from === 'puzzles') { - return ( -
    -
    -
    - -
    - -
    + return ( +
    +
    +
    - ); - } - - return ; +
    + ); }; -export default PebblePlacementGamePage; \ No newline at end of file +export default PebblePlacementGamePage; diff --git a/src/pages/PresentsPuzzlePage.tsx b/src/pages/PresentsPuzzlePage.tsx index cc51817..edb710a 100644 --- a/src/pages/PresentsPuzzlePage.tsx +++ b/src/pages/PresentsPuzzlePage.tsx @@ -1,13 +1,12 @@ -import Layout from '@/components/Layout'; import PresentsPuzzle from "@/components/PresentsPuzzle"; const PresentsPuzzlePage = () => { return ( - -
    +
    +
    - +
    ); }; diff --git a/src/pages/RentDivisionPuzzlePage.tsx b/src/pages/RentDivisionPuzzlePage.tsx index 63dad7f..154033f 100644 --- a/src/pages/RentDivisionPuzzlePage.tsx +++ b/src/pages/RentDivisionPuzzlePage.tsx @@ -1,34 +1,10 @@ -import { useState } from "react"; import RentDivisionPuzzle from "@/components/RentDivisionPuzzle"; -import SocialShare from "@/components/SocialShare"; const RentDivisionPuzzlePage = () => { - const [isCompleted, setIsCompleted] = useState(false); - return ( -
    -
    -
    -

    - Envy-Free Rent Division -

    -

    - Explore fair division by assigning rooms and setting rents. Can you find an - allocation where no one envies another? -

    -
    - - - - +
    +
    +
    ); diff --git a/src/pages/RulesOfInferencePlaygroundPage.tsx b/src/pages/RulesOfInferencePlaygroundPage.tsx index 8c9a2f8..34a2fd3 100644 --- a/src/pages/RulesOfInferencePlaygroundPage.tsx +++ b/src/pages/RulesOfInferencePlaygroundPage.tsx @@ -1,34 +1,13 @@ -import { useState } from "react"; import RulesOfInferencePlayground from "@/components/RulesOfInferencePlayground"; -import SocialShare from "@/components/SocialShare"; const RulesOfInferencePlaygroundPage = () => { - const [isCompleted, setIsCompleted] = useState(false); - return ( -
    -
    -
    -

    Rules of Inference Playground

    -

    - Practice applying resolution and equivalence rules to derive conclusions step by step. -

    -
    - - - - +
    +
    +
    ); }; -export default RulesOfInferencePlaygroundPage; \ No newline at end of file +export default RulesOfInferencePlaygroundPage; diff --git a/src/pages/StackingBlocksPage.tsx b/src/pages/StackingBlocksPage.tsx index 7239dfc..ccb4f78 100644 --- a/src/pages/StackingBlocksPage.tsx +++ b/src/pages/StackingBlocksPage.tsx @@ -1,29 +1,13 @@ import StackingBlocks from "@/components/StackingBlocks"; -import { useSearchParams } from "react-router-dom"; const StackingBlocksPage = () => { - const [searchParams] = useSearchParams(); - const from = searchParams.get('from'); - - if (from === 'puzzles') { - return ( -
    -
    -
    - -
    - -
    + return ( +
    +
    +
    - ); - } - - return ; +
    + ); }; -export default StackingBlocksPage; \ No newline at end of file +export default StackingBlocksPage; diff --git a/src/pages/SubtractionGamePage.tsx b/src/pages/SubtractionGamePage.tsx index 302ca9c..75c3d78 100644 --- a/src/pages/SubtractionGamePage.tsx +++ b/src/pages/SubtractionGamePage.tsx @@ -1,7 +1,13 @@ import SubtractionGame from '@/components/SubtractionGame'; const SubtractionGamePage = () => { - return ; + return ( +
    +
    + +
    +
    + ); }; -export default SubtractionGamePage; \ No newline at end of file +export default SubtractionGamePage; diff --git a/src/pages/TernaryNumberGamePage.tsx b/src/pages/TernaryNumberGamePage.tsx index 79ba882..a85620d 100644 --- a/src/pages/TernaryNumberGamePage.tsx +++ b/src/pages/TernaryNumberGamePage.tsx @@ -2,12 +2,12 @@ import TernaryNumberGame from '@/components/TernaryNumberGame'; const TernaryNumberGamePage = () => { return ( -
    -
    - +
    +
    +
    ); }; -export default TernaryNumberGamePage; \ No newline at end of file +export default TernaryNumberGamePage; diff --git a/src/pages/ZeckendorfGamePage.tsx b/src/pages/ZeckendorfGamePage.tsx index 130ecfa..c585236 100644 --- a/src/pages/ZeckendorfGamePage.tsx +++ b/src/pages/ZeckendorfGamePage.tsx @@ -2,12 +2,12 @@ import ZeckendorfGame from '@/components/ZeckendorfGame'; const ZeckendorfGamePage = () => { return ( -
    -
    - +
    +
    +
    ); }; -export default ZeckendorfGamePage; \ No newline at end of file +export default ZeckendorfGamePage; \ No newline at end of file diff --git a/src/pages/ZeckendorfSearchTrickPage.tsx b/src/pages/ZeckendorfSearchTrickPage.tsx index 59e977f..f5cc437 100644 --- a/src/pages/ZeckendorfSearchTrickPage.tsx +++ b/src/pages/ZeckendorfSearchTrickPage.tsx @@ -2,12 +2,12 @@ import ZeckendorfSearchTrick from '@/components/ZeckendorfSearchTrick'; const ZeckendorfSearchTrickPage = () => { return ( -
    -
    - +
    +
    +
    ); }; -export default ZeckendorfSearchTrickPage; \ No newline at end of file +export default ZeckendorfSearchTrickPage; \ No newline at end of file From aff14bdf2f87793cb16e2d8f1d787dc2368edb73 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:27:43 +0000 Subject: [PATCH 6/7] Remove duplicate SocialShare from Foundations theme page Components now render SocialShare internally via showSocialShare prop, so the page-level SocialShare block was causing duplicates. Co-Authored-By: Neeldhara Misra --- src/pages/theme-pages/discrete-math/Foundations.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/pages/theme-pages/discrete-math/Foundations.tsx b/src/pages/theme-pages/discrete-math/Foundations.tsx index c4eba6c..c8eec1b 100644 --- a/src/pages/theme-pages/discrete-math/Foundations.tsx +++ b/src/pages/theme-pages/discrete-math/Foundations.tsx @@ -3,7 +3,6 @@ import { Link } from "react-router-dom"; import { Search } from "lucide-react"; import Layout from "@/components/Layout"; import InteractiveCard from "@/components/InteractiveCard"; -import SocialShare from "@/components/SocialShare"; import BinaryNumberGame from "@/components/BinaryNumberGame"; import TernaryNumberGame from "@/components/TernaryNumberGame"; import BalancedTernaryGame from "@/components/BalancedTernaryGame"; @@ -80,12 +79,6 @@ const Foundations = () => {
    - -
    ; } @@ -117,4 +110,4 @@ const Foundations = () => {
    ; }; -export default Foundations; \ No newline at end of file +export default Foundations; From 3d31dc1b5540972d67ef03522ba3a02573e1eedc Mon Sep 17 00:00:00 2001 From: Neeldhara Misra Date: Mon, 20 Jul 2026 14:22:11 +0530 Subject: [PATCH 7/7] Add Forgejo CI and Dokploy deployment --- .dockerignore | 8 ++++ .forgejo/workflows/ci.yml | 41 +++++++++++++++++++ Dockerfile | 19 +++++++++ README.md | 6 +++ nginx.conf | 22 ++++++++++ src/components/InteractiveGallery.tsx | 4 +- src/components/InteractiveIndex.tsx | 4 +- src/components/LadybugClockPuzzle.tsx | 4 +- src/components/ZeckendorfGame.tsx | 4 +- .../guessing-game/QuestionGuessingMode.tsx | 3 +- src/components/ui/command.tsx | 2 +- src/components/ui/textarea.tsx | 3 +- tailwind.config.ts | 3 +- 13 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 .dockerignore create mode 100644 .forgejo/workflows/ci.yml create mode 100644 Dockerfile create mode 100644 nginx.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e9a45b4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +.github +.forgejo +dist +node_modules +npm-debug.log* +Dockerfile* +README.md diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml new file mode 100644 index 0000000..40a10b9 --- /dev/null +++ b/.forgejo/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ forgejo.ref }} + cancel-in-progress: true + +jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Build + run: npm run build diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ff67f4c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM node:22-alpine AS build + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY . . +RUN npm run build + +FROM nginx:1.27-alpine AS runtime + +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html + +EXPOSE 80 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -q --spider http://127.0.0.1/ || exit 1 diff --git a/README.md b/README.md index e0eb78a..672b6b0 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,12 @@ This project is built with: Simply open [Lovable](https://lovable.dev/projects/5ee9d1e7-af25-47e9-ab4c-bdbca4554e32) and click on Share -> Publish. +### Forgejo and Dokploy + +Pushes and pull requests targeting `main` are verified by the Forgejo Actions workflow in `.forgejo/workflows/ci.yml`. The production container is built from `Dockerfile` and serves the Vite output with NGINX, including React Router fallback handling. + +Dokploy is connected to the `Websites/interactives` Forgejo repository with autodeploy enabled. A successful push to `main` triggers a fresh Dockerfile build and replaces the running service only after the image has built successfully. + ## Can I connect a custom domain to my Lovable project? Yes, you can! diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..aca9d71 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,22 @@ +server { + listen 80; + listen [::]:80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location = /index.html { + add_header Cache-Control "no-cache"; + } + + location ~* \.(?:css|js|mjs|jpg|jpeg|png|gif|ico|svg|webp|woff|woff2)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } +} diff --git a/src/components/InteractiveGallery.tsx b/src/components/InteractiveGallery.tsx index 1086712..24a3a03 100644 --- a/src/components/InteractiveGallery.tsx +++ b/src/components/InteractiveGallery.tsx @@ -67,7 +67,7 @@ const InteractiveGallery = () => {

    Data Structures and Algorithms

    -

    Interactive explorations of elementary data structures and algorithms.

    +

    Interactive explorations of elementary data structures and algorithms.

    {/* Search bar */} @@ -103,4 +103,4 @@ const InteractiveGallery = () => { ); }; -export default InteractiveGallery; \ No newline at end of file +export default InteractiveGallery; diff --git a/src/components/InteractiveIndex.tsx b/src/components/InteractiveIndex.tsx index 3017d22..4223abe 100644 --- a/src/components/InteractiveIndex.tsx +++ b/src/components/InteractiveIndex.tsx @@ -323,7 +323,7 @@ const InteractiveIndex = () => { // Filter and sort interactives const filteredAndSortedInteractives = useMemo(() => { - let filtered = allInteractives.filter(interactive => { + const filtered = allInteractives.filter(interactive => { const matchesSearch = interactive.title.toLowerCase().includes(searchTerm.toLowerCase()) || interactive.description.toLowerCase().includes(searchTerm.toLowerCase()) || interactive.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase())); @@ -549,4 +549,4 @@ const InteractiveIndex = () => { ); }; -export default InteractiveIndex; \ No newline at end of file +export default InteractiveIndex; diff --git a/src/components/LadybugClockPuzzle.tsx b/src/components/LadybugClockPuzzle.tsx index 4ad2044..7de133f 100644 --- a/src/components/LadybugClockPuzzle.tsx +++ b/src/components/LadybugClockPuzzle.tsx @@ -144,7 +144,7 @@ const LadybugClockPuzzle: React.FC = ({ // Run to completion const runToCompletion = useCallback(() => { setIsPlaying(false); - let currentSim = simulation; + const currentSim = simulation; let position = currentSim.position; const visited = new Set(currentSim.visited); let moveCount = currentSim.moveCount; @@ -545,4 +545,4 @@ const LadybugClockPuzzle: React.FC = ({ {showSocialShare && }
    ; }; -export default LadybugClockPuzzle; \ No newline at end of file +export default LadybugClockPuzzle; diff --git a/src/components/ZeckendorfGame.tsx b/src/components/ZeckendorfGame.tsx index 439b0d0..1655e46 100644 --- a/src/components/ZeckendorfGame.tsx +++ b/src/components/ZeckendorfGame.tsx @@ -34,7 +34,7 @@ const ZeckendorfGame: React.FC = ({ showSocialShare = true // Generate a random combination of Fibonacci numbers const numTerms = Math.floor(Math.random() * 4) + 2; // 2-5 terms let target = 0; - let usedIndices = new Set(); + const usedIndices = new Set(); for (let i = 0; i < numTerms; i++) { let index; @@ -235,4 +235,4 @@ const ZeckendorfGame: React.FC = ({ showSocialShare = true ); }; -export default ZeckendorfGame; \ No newline at end of file +export default ZeckendorfGame; diff --git a/src/components/guessing-game/QuestionGuessingMode.tsx b/src/components/guessing-game/QuestionGuessingMode.tsx index d30855a..d76a11a 100644 --- a/src/components/guessing-game/QuestionGuessingMode.tsx +++ b/src/components/guessing-game/QuestionGuessingMode.tsx @@ -217,7 +217,7 @@ const QuestionGuessingMode: React.FC = ({ case 'odd': baseCondition = (num % 2 !== 0); break; - case 'prime': + case 'prime': { const isPrimeNum = (n: number): boolean => { if (n < 2) return false; for (let i = 2; i <= Math.sqrt(n); i++) { @@ -227,6 +227,7 @@ const QuestionGuessingMode: React.FC = ({ }; baseCondition = isPrimeNum(num); break; + } case 'perfectSquare': baseCondition = Math.sqrt(num) % 1 === 0; break; diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx index 56a0979..69d63a4 100644 --- a/src/components/ui/command.tsx +++ b/src/components/ui/command.tsx @@ -21,7 +21,7 @@ const Command = React.forwardRef< )) Command.displayName = CommandPrimitive.displayName -interface CommandDialogProps extends DialogProps {} +type CommandDialogProps = DialogProps const CommandDialog = ({ children, ...props }: CommandDialogProps) => { return ( diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx index 9f9a6dc..12c9136 100644 --- a/src/components/ui/textarea.tsx +++ b/src/components/ui/textarea.tsx @@ -2,8 +2,7 @@ import * as React from "react" import { cn } from "@/lib/utils" -export interface TextareaProps - extends React.TextareaHTMLAttributes {} +export type TextareaProps = React.TextareaHTMLAttributes const Textarea = React.forwardRef( ({ className, ...props }, ref) => { diff --git a/tailwind.config.ts b/tailwind.config.ts index a4b00a3..a934352 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,4 +1,5 @@ import type { Config } from "tailwindcss"; +import tailwindcssAnimate from "tailwindcss-animate"; export default { darkMode: ["class"], @@ -122,5 +123,5 @@ export default { } } }, - plugins: [require("tailwindcss-animate")], + plugins: [tailwindcssAnimate], } satisfies Config;