Add Ternary Search Magic Trick interactive and fix layout issues
- Create new Ternary Search Magic Trick interactive with ternary search logic - Add emoji buttons and 'not present' option for better UX - Implement 'How it Works' modal with embedded YouTube video - Fix layout margins when accessing interactives from theme pages - Add standalone and green screen pages for the new interactive - Update Data Structures theme to include the new interactive
This commit is contained in:
parent
3f04e8c7d7
commit
7451682dd5
7 changed files with 419 additions and 26 deletions
|
|
@ -22,6 +22,8 @@ import BinarySearchTrickPage from "./pages/BinarySearchTrickPage";
|
|||
import BinarySearchTrickGreenScreen from "./pages/BinarySearchTrickGreenScreen";
|
||||
import GameOfSimGreenScreen from "./pages/GameOfSimGreenScreen";
|
||||
import GameOfSimPage from "./pages/GameOfSimPage";
|
||||
import TernarySearchTrickPage from "./pages/TernarySearchTrickPage";
|
||||
import TernarySearchTrickGreenScreen from "./pages/TernarySearchTrickGreenScreen";
|
||||
import Foundations from "./pages/theme-pages/discrete-math/Foundations";
|
||||
import Proofs from "./pages/theme-pages/discrete-math/Proofs";
|
||||
import Counting from "./pages/theme-pages/discrete-math/Counting";
|
||||
|
|
@ -70,6 +72,8 @@ const App = () => (
|
|||
<Route path="/binary-search-trick/:mode" element={<BinarySearchTrickGreenScreen />} />
|
||||
<Route path="/game-of-sim" element={<GameOfSimPage />} />
|
||||
<Route path="/game-of-sim/:mode" element={<GameOfSimGreenScreen />} />
|
||||
<Route path="/ternary-search-trick" element={<TernarySearchTrickPage />} />
|
||||
<Route path="/ternary-search-trick/:mode" element={<TernarySearchTrickGreenScreen />} />
|
||||
|
||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
|
|
|
|||
|
|
@ -3,38 +3,60 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
|||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Search } from 'lucide-react';
|
||||
import Layout from '@/components/Layout';
|
||||
import BinarySearchTrick from '@/components/BinarySearchTrick';
|
||||
import TernarySearchTrick from '@/components/TernarySearchTrick';
|
||||
|
||||
interface Interactive {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
component: React.ComponentType;
|
||||
component: React.ComponentType<{ showSocialShare?: boolean }>;
|
||||
}
|
||||
const interactives: Interactive[] = [{
|
||||
|
||||
const interactives: Interactive[] = [
|
||||
{
|
||||
id: 'binary-search-trick',
|
||||
title: 'Binary Search Magic Trick',
|
||||
description: 'Perform an amazing magic trick that reveals how binary numbers work. Think of a number and watch the magic happen!',
|
||||
tags: ['binary', 'magic', 'numbers', 'trick', 'data-structures'],
|
||||
component: BinarySearchTrick
|
||||
}];
|
||||
},
|
||||
{
|
||||
id: 'ternary-search-trick',
|
||||
title: 'Ternary Search Magic Trick',
|
||||
description: 'Experience the magic of ternary search! Think of a number and watch as the cards reveal it through mathematical calculations.',
|
||||
tags: ['ternary', 'magic', 'numbers', 'trick', 'data-structures', 'search'],
|
||||
component: TernarySearchTrick
|
||||
}
|
||||
];
|
||||
const InteractiveGallery = () => {
|
||||
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;
|
||||
return <div className="space-y-6">
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="container mx-auto px-4 py-8 space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<button onClick={() => setSelectedInteractive(null)} className="text-primary hover:text-primary/80 font-medium">← Back to Data Structures and Algorithms</button>
|
||||
|
||||
<button
|
||||
onClick={() => setSelectedInteractive(null)}
|
||||
className="text-primary hover:text-primary/80 font-medium"
|
||||
>
|
||||
← Back to Data Structures and Algorithms
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-background border rounded-lg p-6">
|
||||
<InteractiveComponent />
|
||||
<InteractiveComponent showSocialShare={true} />
|
||||
</div>
|
||||
</div>;
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <div className="space-y-6">
|
||||
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">Data Structures and Algorithms</h1>
|
||||
<p className="text-muted-foreground text-lg">Interactive explorations of elementary data structures and algorithms.</p>
|
||||
|
|
@ -68,6 +90,9 @@ const InteractiveGallery = () => {
|
|||
{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>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
export default InteractiveGallery;
|
||||
|
|
@ -45,6 +45,15 @@ const allInteractives: Interactive[] = [
|
|||
path: '/game-of-sim',
|
||||
theme: 'Games',
|
||||
dateAdded: '2024-03-10'
|
||||
},
|
||||
{
|
||||
id: 'ternary-search-trick',
|
||||
title: 'Ternary Search Magic Trick',
|
||||
description: 'Experience the magic of ternary search! Think of a number and watch as the cards reveal it through mathematical calculations.',
|
||||
tags: ['ternary', 'magic', 'numbers', 'trick', 'data-structures', 'search'],
|
||||
path: '/ternary-search-trick',
|
||||
theme: 'Data Structures',
|
||||
dateAdded: '2024-07-20'
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||
331
src/components/TernarySearchTrick.tsx
Normal file
331
src/components/TernarySearchTrick.tsx
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
import React, { 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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { RotateCcw, Wand2, Info } from 'lucide-react';
|
||||
import SocialShare from '@/components/SocialShare';
|
||||
|
||||
interface TernarySearchTrickProps {
|
||||
showSocialShare?: boolean;
|
||||
}
|
||||
|
||||
const TernarySearchTrick: React.FC<TernarySearchTrickProps> = ({ showSocialShare = true }) => {
|
||||
const [step, setStep] = useState<'cards' | 'final-question' | 'result'>('cards');
|
||||
const [cardSelections, setCardSelections] = useState<{
|
||||
card1: 'left' | 'right' | 'missing' | null;
|
||||
card2: 'left' | 'right' | 'missing' | null;
|
||||
card3: 'left' | 'right' | 'missing' | null;
|
||||
}>({
|
||||
card1: null,
|
||||
card2: null,
|
||||
card3: null
|
||||
});
|
||||
const [isBiggerThan13, setIsBiggerThan13] = useState<boolean | null>(null);
|
||||
const [calculatedNumber, setCalculatedNumber] = useState<number | null>(null);
|
||||
|
||||
const cardData = {
|
||||
card1: {
|
||||
left: [5, 6, 7, 8, 9, 10, 11, 12, 13, 32, 33, 34, 35, 36, 37, 38, 39, 40],
|
||||
right: [14, 15, 16, 17, 18, 19, 20, 21, 22]
|
||||
},
|
||||
card2: {
|
||||
left: [2, 3, 4, 11, 12, 13, 20, 21, 22, 29, 30, 31, 38, 39, 40],
|
||||
right: [5, 6, 7, 14, 15, 16, 23, 24, 25, 32, 33, 34]
|
||||
},
|
||||
card3: {
|
||||
left: [1, 4, 7, 10, 13, 16, 19, 22, 25, 28, 31, 34, 37, 40],
|
||||
right: [2, 5, 8, 11, 14, 17, 20, 23, 26, 29, 32, 35, 38]
|
||||
}
|
||||
};
|
||||
|
||||
const resetTrick = () => {
|
||||
setStep('cards');
|
||||
setCardSelections({
|
||||
card1: null,
|
||||
card2: null,
|
||||
card3: null
|
||||
});
|
||||
setIsBiggerThan13(null);
|
||||
setCalculatedNumber(null);
|
||||
};
|
||||
|
||||
const handleCardSelection = (cardNumber: 1 | 2 | 3, side: 'left' | 'right' | 'missing') => {
|
||||
setCardSelections(prev => ({
|
||||
...prev,
|
||||
[`card${cardNumber}`]: side
|
||||
}));
|
||||
};
|
||||
|
||||
const handleContinue = () => {
|
||||
if (cardSelections.card1 && cardSelections.card2 && cardSelections.card3) {
|
||||
setStep('final-question');
|
||||
}
|
||||
};
|
||||
|
||||
const handleFinalAnswer = (biggerThan13: boolean) => {
|
||||
setIsBiggerThan13(biggerThan13);
|
||||
|
||||
// Calculate the number
|
||||
let guess = 0;
|
||||
|
||||
// Card 1: left adds 9, right subtracts 9, missing adds 0
|
||||
if (cardSelections.card1 === 'left') guess += 9;
|
||||
else if (cardSelections.card1 === 'right') guess -= 9;
|
||||
// missing adds 0, so no change
|
||||
|
||||
// Card 2: left adds 3, right subtracts 3, missing adds 0
|
||||
if (cardSelections.card2 === 'left') guess += 3;
|
||||
else if (cardSelections.card2 === 'right') guess -= 3;
|
||||
// missing adds 0, so no change
|
||||
|
||||
// Card 3: left adds 1, right subtracts 1, missing adds 0
|
||||
if (cardSelections.card3 === 'left') guess += 1;
|
||||
else if (cardSelections.card3 === 'right') guess -= 1;
|
||||
// missing adds 0, so no change
|
||||
|
||||
// If bigger than 13, add 27
|
||||
if (biggerThan13) guess += 27;
|
||||
|
||||
setCalculatedNumber(guess);
|
||||
setStep('result');
|
||||
};
|
||||
|
||||
const renderCard = (cardNumber: 1 | 2 | 3) => {
|
||||
const cardKey = `card${cardNumber}` as keyof typeof cardData;
|
||||
const isSelected = cardSelections[cardKey as keyof typeof cardSelections];
|
||||
|
||||
return (
|
||||
<Card className={`w-full max-w-md ${isSelected ? 'ring-2 ring-primary' : ''}`}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center">Card {cardNumber}</CardTitle>
|
||||
<CardDescription className="text-center">
|
||||
{isSelected ? `You selected: ${isSelected === 'missing' ? 'not present' : isSelected}` : 'Choose where your number appears'}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Emoji Buttons at Top */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="text-center">
|
||||
<Button
|
||||
variant={isSelected === 'left' ? 'default' : 'outline'}
|
||||
size="lg"
|
||||
className="w-full h-16 text-2xl"
|
||||
onClick={() => handleCardSelection(cardNumber, 'left')}
|
||||
>
|
||||
⬅️
|
||||
</Button>
|
||||
<p className="text-sm font-medium mt-2">Left</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<Button
|
||||
variant={isSelected === 'missing' ? 'default' : 'outline'}
|
||||
size="lg"
|
||||
className="w-full h-16 text-2xl"
|
||||
onClick={() => handleCardSelection(cardNumber, 'missing')}
|
||||
>
|
||||
❌
|
||||
</Button>
|
||||
<p className="text-sm font-medium mt-2">Not Present</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<Button
|
||||
variant={isSelected === 'right' ? 'default' : 'outline'}
|
||||
size="lg"
|
||||
className="w-full h-16 text-2xl"
|
||||
onClick={() => handleCardSelection(cardNumber, 'right')}
|
||||
>
|
||||
➡️
|
||||
</Button>
|
||||
<p className="text-sm font-medium mt-2">Right</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Number Boxes at Bottom */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="bg-muted p-3 rounded-lg text-xs">
|
||||
{cardData[cardKey].left.map(num => (
|
||||
<span key={num} className="inline-block w-6 text-center">{num}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="bg-muted p-3 rounded-lg text-xs">
|
||||
{cardData[cardKey].right.map(num => (
|
||||
<span key={num} className="inline-block w-6 text-center">{num}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFinalQuestion = () => (
|
||||
<Card className="w-full max-w-md mx-auto">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center">Final Question</CardTitle>
|
||||
<CardDescription className="text-center">
|
||||
Is your number bigger than 13?
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() => handleFinalAnswer(true)}
|
||||
>
|
||||
Yes, bigger than 13
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() => handleFinalAnswer(false)}
|
||||
>
|
||||
No, 13 or smaller
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const renderResult = () => (
|
||||
<Card className="w-full max-w-md mx-auto">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center flex items-center justify-center gap-2">
|
||||
<Wand2 className="w-6 h-6" />
|
||||
Magic Result!
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-center space-y-4">
|
||||
<div className="text-6xl font-bold text-primary">
|
||||
{calculatedNumber}
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
That's your number! 🎉
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl font-bold text-center">Ternary Search Magic Trick</CardTitle>
|
||||
<CardDescription className="text-center">
|
||||
Think of a number between 1 and 40, then follow the magic cards to reveal your number!
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Step Indicator */}
|
||||
<div className="flex justify-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={step === 'cards' ? 'default' : 'secondary'}>1. Cards</Badge>
|
||||
<span className="text-muted-foreground">→</span>
|
||||
<Badge variant={step === 'final-question' ? 'default' : 'secondary'}>2. Question</Badge>
|
||||
<span className="text-muted-foreground">→</span>
|
||||
<Badge variant={step === 'result' ? 'default' : 'secondary'}>3. Result</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content based on step */}
|
||||
{step === 'cards' && (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{renderCard(1)}
|
||||
{renderCard(2)}
|
||||
{renderCard(3)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
onClick={handleContinue}
|
||||
disabled={!cardSelections.card1 || !cardSelections.card2 || !cardSelections.card3}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
Continue to Final Question
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'final-question' && renderFinalQuestion()}
|
||||
{step === 'result' && renderResult()}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-center gap-4">
|
||||
<Button variant="outline" onClick={resetTrick} className="flex items-center gap-2">
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
Start Over
|
||||
</Button>
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="flex items-center gap-2">
|
||||
<Info className="w-4 h-4" />
|
||||
How it Works
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Info className="w-5 h-5" />
|
||||
How the Ternary Search Magic Trick Works
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<p className="text-muted-foreground">
|
||||
This trick uses the balanced ternary representation of numbers, about which you can learn more in the following video:
|
||||
</p>
|
||||
<div className="flex justify-center">
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
src="https://www.youtube.com/embed/yfQVL0_POKg?si=h52dE5LtTb7BWoHG"
|
||||
title="YouTube video player"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
allowFullScreen
|
||||
className="w-full max-w-full rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="text-center space-y-2 p-4 bg-muted rounded-lg">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Think of a number between 1 and 40, then select which side of each card contains your number.
|
||||
</p>
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
The magic will reveal your number through mathematical calculations!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Social Share */}
|
||||
{showSocialShare && (
|
||||
<div className="flex justify-center">
|
||||
<SocialShare
|
||||
title="Ternary Search Magic Trick"
|
||||
description="Experience the magic of ternary search! Think of a number and watch as the cards reveal it through mathematical calculations."
|
||||
url={`${window.location.origin}/ternary-search-trick`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TernarySearchTrick;
|
||||
18
src/pages/TernarySearchTrickGreenScreen.tsx
Normal file
18
src/pages/TernarySearchTrickGreenScreen.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { useParams } from 'react-router-dom';
|
||||
import TernarySearchTrick from '@/components/TernarySearchTrick';
|
||||
import GreenScreenWrapper from '@/components/GreenScreenWrapper';
|
||||
|
||||
const TernarySearchTrickGreenScreen = () => {
|
||||
const { mode } = useParams<{ mode: 'gs-lite' | 'gs-dark' }>();
|
||||
const greenScreenMode = mode === 'gs-dark' ? 'dark' : 'lite';
|
||||
|
||||
return (
|
||||
<GreenScreenWrapper mode={greenScreenMode}>
|
||||
<div className="space-y-6">
|
||||
<TernarySearchTrick showSocialShare={false} />
|
||||
</div>
|
||||
</GreenScreenWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default TernarySearchTrickGreenScreen;
|
||||
13
src/pages/TernarySearchTrickPage.tsx
Normal file
13
src/pages/TernarySearchTrickPage.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import TernarySearchTrick from '@/components/TernarySearchTrick';
|
||||
|
||||
const TernarySearchTrickPage = () => {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<div className="w-full max-w-6xl px-4 py-8">
|
||||
<TernarySearchTrick showSocialShare={true} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TernarySearchTrickPage;
|
||||
|
|
@ -1,14 +1,7 @@
|
|||
import Layout from "@/components/Layout";
|
||||
import InteractiveGallery from "@/components/InteractiveGallery";
|
||||
|
||||
const DataStructures = () => {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="max-w-7xl mx-auto p-6">
|
||||
<InteractiveGallery />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
return <InteractiveGallery />;
|
||||
};
|
||||
|
||||
export default DataStructures;
|
||||
Loading…
Add table
Add a link
Reference in a new issue