Add Presents Puzzle interactive to Discrete Math > Uncertainty
Created a new interactive puzzle that explores probability and search strategies: - Alice opens boxes in sequential order (1, 2, 3, ...) - Bob opens odds first, then evens (1, 3, 5, ..., 2, 4, 6, ...) - 26 presents randomly distributed in 100 boxes - Interactive visualization with two 10×10 grids - Single simulation with animated opening process - Batch simulation of 100 rounds to analyze win rates - Added to Uncertainty theme page (first interactive in this category)
This commit is contained in:
parent
eccdabf64d
commit
d250cfbca9
5 changed files with 480 additions and 6 deletions
|
|
@ -59,6 +59,7 @@ import CrapsGamePage from './pages/CrapsGamePage';
|
||||||
import NeighborSumAvoidancePage from './pages/NeighborSumAvoidancePage';
|
import NeighborSumAvoidancePage from './pages/NeighborSumAvoidancePage';
|
||||||
import BurnsidesLemmaPage from './pages/BurnsidesLemmaPage';
|
import BurnsidesLemmaPage from './pages/BurnsidesLemmaPage';
|
||||||
import CubeColoringPage from './pages/CubeColoringPage';
|
import CubeColoringPage from './pages/CubeColoringPage';
|
||||||
|
import PresentsPuzzlePage from './pages/PresentsPuzzlePage';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
|
@ -87,6 +88,7 @@ const App = () => (
|
||||||
<Route path="/themes/discrete-math/graphs/neighbor-sum-avoidance" element={<NeighborSumAvoidancePage />} />
|
<Route path="/themes/discrete-math/graphs/neighbor-sum-avoidance" element={<NeighborSumAvoidancePage />} />
|
||||||
<Route path="/themes/discrete-math/structures/burnsides-lemma" element={<BurnsidesLemmaPage />} />
|
<Route path="/themes/discrete-math/structures/burnsides-lemma" element={<BurnsidesLemmaPage />} />
|
||||||
<Route path="/themes/discrete-math/structures/cube-coloring" element={<CubeColoringPage />} />
|
<Route path="/themes/discrete-math/structures/cube-coloring" element={<CubeColoringPage />} />
|
||||||
|
<Route path="/themes/discrete-math/uncertainty/presents-puzzle" element={<PresentsPuzzlePage />} />
|
||||||
<Route path="/themes/social-choice" element={<SocialChoice />} />
|
<Route path="/themes/social-choice" element={<SocialChoice />} />
|
||||||
<Route path="/themes/advanced-algorithms" element={<AdvancedAlgorithms />} />
|
<Route path="/themes/advanced-algorithms" element={<AdvancedAlgorithms />} />
|
||||||
<Route path="/themes/data-structures" element={<DataStructures />} />
|
<Route path="/themes/data-structures" element={<DataStructures />} />
|
||||||
|
|
|
||||||
|
|
@ -270,6 +270,15 @@ const allInteractives: Interactive[] = [
|
||||||
path: '/themes/discrete-math/structures/cube-coloring',
|
path: '/themes/discrete-math/structures/cube-coloring',
|
||||||
theme: 'Discrete Math',
|
theme: 'Discrete Math',
|
||||||
dateAdded: '2025-01-22'
|
dateAdded: '2025-01-22'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'presents-puzzle',
|
||||||
|
title: 'The Presents Puzzle',
|
||||||
|
description: 'Explore a probability puzzle about search strategies. Charlie puts 26 presents in 100 boxes. Alice opens them in order while Bob opens odds first, then evens. Who is more likely to see all 26 presents first?',
|
||||||
|
tags: ['probability', 'search', 'simulation', 'expected-value', 'strategy', 'uncertainty'],
|
||||||
|
path: '/themes/discrete-math/uncertainty/presents-puzzle',
|
||||||
|
theme: 'Discrete Math',
|
||||||
|
dateAdded: '2025-11-19'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
349
src/components/PresentsPuzzle.tsx
Normal file
349
src/components/PresentsPuzzle.tsx
Normal file
|
|
@ -0,0 +1,349 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import SocialShare from './SocialShare';
|
||||||
|
import { Gift, Play, RotateCw, Zap } from 'lucide-react';
|
||||||
|
|
||||||
|
interface PresentsPuzzleProps {
|
||||||
|
showSocialShare?: boolean;
|
||||||
|
shareUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SimulationResult {
|
||||||
|
alice: number;
|
||||||
|
bob: number;
|
||||||
|
ties: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PresentsPuzzle = ({ showSocialShare = false, shareUrl }: PresentsPuzzleProps) => {
|
||||||
|
const [presentBoxes, setPresentBoxes] = useState<Set<number>>(new Set());
|
||||||
|
const [aliceOpened, setAliceOpened] = useState<Set<number>>(new Set());
|
||||||
|
const [bobOpened, setBobOpened] = useState<Set<number>>(new Set());
|
||||||
|
const [aliceFound, setAliceFound] = useState(0);
|
||||||
|
const [bobFound, setBobFound] = useState(0);
|
||||||
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
|
const [winner, setWinner] = useState<'alice' | 'bob' | null>(null);
|
||||||
|
const [simulationResults, setSimulationResults] = useState<SimulationResult>({ alice: 0, bob: 0, ties: 0 });
|
||||||
|
const [hasSimulated, setHasSimulated] = useState(false);
|
||||||
|
|
||||||
|
// Generate random present positions
|
||||||
|
const generatePresents = (): Set<number> => {
|
||||||
|
const presents = new Set<number>();
|
||||||
|
while (presents.size < 26) {
|
||||||
|
presents.add(Math.floor(Math.random() * 100) + 1);
|
||||||
|
}
|
||||||
|
return presents;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get Alice's opening order: 1, 2, 3, ..., 100
|
||||||
|
const getAliceOrder = (): number[] => {
|
||||||
|
return Array.from({ length: 100 }, (_, i) => i + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get Bob's opening order: 1, 3, 5, ..., 99, 2, 4, 6, ..., 100
|
||||||
|
const getBobOrder = (): number[] => {
|
||||||
|
const odds = Array.from({ length: 50 }, (_, i) => i * 2 + 1);
|
||||||
|
const evens = Array.from({ length: 50 }, (_, i) => (i + 1) * 2);
|
||||||
|
return [...odds, ...evens];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simulate one round
|
||||||
|
const simulateRound = (presents: Set<number>): 'alice' | 'bob' => {
|
||||||
|
const aliceOrder = getAliceOrder();
|
||||||
|
const bobOrder = getBobOrder();
|
||||||
|
|
||||||
|
let aliceCount = 0;
|
||||||
|
let bobCount = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
if (presents.has(aliceOrder[i])) {
|
||||||
|
aliceCount++;
|
||||||
|
if (aliceCount === 26) return 'alice';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (presents.has(bobOrder[i])) {
|
||||||
|
bobCount++;
|
||||||
|
if (bobCount === 26) return 'bob';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'alice'; // Default, though this should never happen
|
||||||
|
};
|
||||||
|
|
||||||
|
// Animate a single simulation
|
||||||
|
const startSimulation = async () => {
|
||||||
|
setIsRunning(true);
|
||||||
|
setWinner(null);
|
||||||
|
setAliceOpened(new Set());
|
||||||
|
setBobOpened(new Set());
|
||||||
|
setAliceFound(0);
|
||||||
|
setBobFound(0);
|
||||||
|
|
||||||
|
const presents = generatePresents();
|
||||||
|
setPresentBoxes(presents);
|
||||||
|
|
||||||
|
const aliceOrder = getAliceOrder();
|
||||||
|
const bobOrder = getBobOrder();
|
||||||
|
|
||||||
|
let aliceCount = 0;
|
||||||
|
let bobCount = 0;
|
||||||
|
let currentWinner: 'alice' | 'bob' | null = null;
|
||||||
|
|
||||||
|
for (let i = 0; i < 100 && !currentWinner; i++) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50)); // Animation delay
|
||||||
|
|
||||||
|
const aliceBox = aliceOrder[i];
|
||||||
|
const bobBox = bobOrder[i];
|
||||||
|
|
||||||
|
setAliceOpened(prev => new Set(prev).add(aliceBox));
|
||||||
|
setBobOpened(prev => new Set(prev).add(bobBox));
|
||||||
|
|
||||||
|
if (presents.has(aliceBox)) {
|
||||||
|
aliceCount++;
|
||||||
|
setAliceFound(aliceCount);
|
||||||
|
if (aliceCount === 26) {
|
||||||
|
currentWinner = 'alice';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (presents.has(bobBox)) {
|
||||||
|
bobCount++;
|
||||||
|
setBobFound(bobCount);
|
||||||
|
if (bobCount === 26 && !currentWinner) {
|
||||||
|
currentWinner = 'bob';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setWinner(currentWinner);
|
||||||
|
setIsRunning(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset simulation
|
||||||
|
const resetSimulation = () => {
|
||||||
|
setPresentBoxes(new Set());
|
||||||
|
setAliceOpened(new Set());
|
||||||
|
setBobOpened(new Set());
|
||||||
|
setAliceFound(0);
|
||||||
|
setBobFound(0);
|
||||||
|
setWinner(null);
|
||||||
|
setIsRunning(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run 100 simulations
|
||||||
|
const runMultipleSimulations = () => {
|
||||||
|
const results: SimulationResult = { alice: 0, bob: 0, ties: 0 };
|
||||||
|
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
const presents = generatePresents();
|
||||||
|
const winner = simulateRound(presents);
|
||||||
|
if (winner === 'alice') {
|
||||||
|
results.alice++;
|
||||||
|
} else {
|
||||||
|
results.bob++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSimulationResults(results);
|
||||||
|
setHasSimulated(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render a grid of boxes
|
||||||
|
const renderGrid = (
|
||||||
|
title: string,
|
||||||
|
openedBoxes: Set<number>,
|
||||||
|
found: number,
|
||||||
|
color: string
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="font-semibold text-lg">{title}</h3>
|
||||||
|
<Badge variant="secondary" className="text-sm">
|
||||||
|
<Gift className="w-3 h-3 mr-1" />
|
||||||
|
{found}/26
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-10 gap-1 p-2 bg-muted/30 rounded-lg">
|
||||||
|
{Array.from({ length: 100 }, (_, i) => i + 1).map((boxNum) => {
|
||||||
|
const hasPresent = presentBoxes.has(boxNum);
|
||||||
|
const isOpened = openedBoxes.has(boxNum);
|
||||||
|
const showPresent = isOpened && hasPresent;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={boxNum}
|
||||||
|
className={`
|
||||||
|
aspect-square rounded border flex items-center justify-center text-xs font-medium
|
||||||
|
transition-all duration-200
|
||||||
|
${isOpened
|
||||||
|
? showPresent
|
||||||
|
? 'bg-[#808000] text-white border-[#606000] shadow-sm'
|
||||||
|
: 'bg-muted border-border text-muted-foreground'
|
||||||
|
: 'bg-gray-400 border-gray-500 text-gray-700'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
title={`Box ${boxNum}${hasPresent ? ' (Present!)' : ''}`}
|
||||||
|
>
|
||||||
|
{showPresent ? <Gift className="w-3 h-3" /> : boxNum}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{title === 'Alice' && 'Opens in order: 1, 2, 3, 4, 5, ...'}
|
||||||
|
{title === 'Bob' && 'Opens odds first, then evens: 1, 3, 5, ..., 2, 4, 6, ...'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Gift className="w-6 h-6" />
|
||||||
|
The Presents Puzzle
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
A probability puzzle about search strategies
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
{/* Puzzle Statement */}
|
||||||
|
<Card className="bg-muted/50">
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<p className="text-sm leading-relaxed">
|
||||||
|
Charlie puts <strong>26 presents</strong> in <strong>100 boxes</strong>, labeled 1 to 100.
|
||||||
|
Each second, Alice and Bob look in one box. Alice opens them in order (1, 2, 3, …),
|
||||||
|
while Bob opens the odds first, then the evens (1, 3, 5, …, 2, 4, 6, …).
|
||||||
|
<strong> Who is more likely to see all 26 presents first?</strong>
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Grids */}
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
{renderGrid('Alice', aliceOpened, aliceFound, 'blue')}
|
||||||
|
{renderGrid('Bob', bobOpened, bobFound, 'green')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Winner Display */}
|
||||||
|
{winner && (
|
||||||
|
<Card className="bg-primary/10 border-primary/20">
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl font-bold text-primary">
|
||||||
|
{winner === 'alice' ? '🎉 Alice' : '🎉 Bob'} found all 26 presents first!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Control Buttons */}
|
||||||
|
<div className="flex gap-3 justify-center flex-wrap">
|
||||||
|
<Button
|
||||||
|
onClick={startSimulation}
|
||||||
|
disabled={isRunning}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<Play className="w-4 h-4" />
|
||||||
|
Start
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={resetSimulation}
|
||||||
|
variant="outline"
|
||||||
|
disabled={isRunning}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<RotateCw className="w-4 h-4" />
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Simulate 100 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Button
|
||||||
|
onClick={runMultipleSimulations}
|
||||||
|
variant="secondary"
|
||||||
|
className="gap-2"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<Zap className="w-4 h-4" />
|
||||||
|
Simulate 100 Rounds
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Results */}
|
||||||
|
{hasSimulated && (
|
||||||
|
<Card className="bg-accent/10 border-accent/20">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Results from 100 Simulations</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="text-center space-y-2">
|
||||||
|
<div className="text-4xl font-bold text-blue-600 dark:text-blue-400">
|
||||||
|
{simulationResults.alice}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-medium">Alice Wins</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{((simulationResults.alice / 100) * 100).toFixed(1)}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center space-y-2">
|
||||||
|
<div className="text-4xl font-bold text-green-600 dark:text-green-400">
|
||||||
|
{simulationResults.bob}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-medium">Bob Wins</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{((simulationResults.bob / 100) * 100).toFixed(1)}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 pt-4 border-t border-border">
|
||||||
|
<p className="text-sm text-muted-foreground text-center">
|
||||||
|
{simulationResults.alice > simulationResults.bob
|
||||||
|
? '💡 Alice tends to win more often!'
|
||||||
|
: simulationResults.bob > simulationResults.alice
|
||||||
|
? '💡 Bob tends to win more often!'
|
||||||
|
: '💡 They appear equally likely to win!'
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Insight */}
|
||||||
|
<Card className="bg-muted/50">
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<h4 className="font-semibold mb-2 text-sm">💭 Think About It</h4>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
At first glance, both strategies seem equivalent—they both check all 100 boxes eventually.
|
||||||
|
But the <em>order</em> matters! Try running the simulation multiple times to see which strategy
|
||||||
|
performs better on average. Can you explain why?
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{showSocialShare && (
|
||||||
|
<SocialShare
|
||||||
|
url={shareUrl || window.location.href}
|
||||||
|
title="The Presents Puzzle"
|
||||||
|
description="A probability puzzle about search strategies and expected value"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PresentsPuzzle;
|
||||||
7
src/pages/PresentsPuzzlePage.tsx
Normal file
7
src/pages/PresentsPuzzlePage.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import PresentsPuzzle from "@/components/PresentsPuzzle";
|
||||||
|
|
||||||
|
const PresentsPuzzlePage = () => {
|
||||||
|
return <PresentsPuzzle />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PresentsPuzzlePage;
|
||||||
|
|
@ -1,11 +1,118 @@
|
||||||
import ComingSoon from "@/components/ComingSoon";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Search } from 'lucide-react';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import Layout from '@/components/Layout';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import PresentsPuzzle from '@/components/PresentsPuzzle';
|
||||||
|
|
||||||
|
interface Interactive {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
tags: string[];
|
||||||
|
path: string;
|
||||||
|
component: React.ComponentType<{ showSocialShare?: boolean; shareUrl?: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const interactives: Interactive[] = [
|
||||||
|
{
|
||||||
|
id: 'presents-puzzle',
|
||||||
|
title: "The Presents Puzzle",
|
||||||
|
description: 'Explore a probability puzzle about search strategies. Charlie puts 26 presents in 100 boxes. Alice opens them in order while Bob opens odds first, then evens. Who is more likely to see all 26 presents first?',
|
||||||
|
tags: ['probability', 'search', 'simulation', 'expected-value', 'strategy'],
|
||||||
|
path: '/themes/discrete-math/uncertainty/presents-puzzle',
|
||||||
|
component: PresentsPuzzle
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const Uncertainty = () => {
|
const Uncertainty = () => {
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [selectedInteractive, setSelectedInteractive] = useState<Interactive | null>(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;
|
||||||
|
const shareUrl = `${window.location.origin}${selectedInteractive.path}`;
|
||||||
return (
|
return (
|
||||||
<ComingSoon
|
<div className="min-h-screen bg-background">
|
||||||
title="Uncertainty"
|
<div className="container mx-auto px-4 py-8 space-y-6">
|
||||||
description="Study probability theory, random variables, and statistical reasoning in discrete contexts through simulations and interactive models."
|
<div className="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedInteractive(null)}
|
||||||
|
className="text-primary hover:text-primary/80 font-medium"
|
||||||
|
>
|
||||||
|
← Back to Uncertainty
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<InteractiveComponent showSocialShare={true} shareUrl={shareUrl} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="max-w-7xl mx-auto p-6">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h1 className="text-3xl font-bold text-foreground">Uncertainty</h1>
|
||||||
|
<p className="text-muted-foreground text-lg">
|
||||||
|
Study probability theory, random variables, and statistical reasoning in discrete contexts through simulations and interactive models.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search bar */}
|
||||||
|
<div className="relative max-w-md">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search interactives..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={e => setSearchTerm(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gallery */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{filteredInteractives.map(interactive => (
|
||||||
|
<Card
|
||||||
|
key={interactive.id}
|
||||||
|
className="cursor-pointer hover:shadow-lg transition-all duration-200 hover:scale-105"
|
||||||
|
onClick={() => setSelectedInteractive(interactive)}
|
||||||
|
>
|
||||||
|
<CardHeader className="space-y-3">
|
||||||
|
<CardTitle className="text-xl text-foreground">{interactive.title}</CardTitle>
|
||||||
|
<CardDescription className="text-muted-foreground line-clamp-3">
|
||||||
|
{interactive.description}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{interactive.tags.map(tag => (
|
||||||
|
<Badge key={tag} variant="secondary" className="text-xs">
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredInteractives.length === 0 && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<p className="text-muted-foreground text-lg">No interactives found matching your search.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue