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:
Neeldhara Misra 2025-07-20 10:29:09 +05:30
parent 3f04e8c7d7
commit 7451682dd5
7 changed files with 419 additions and 26 deletions

View file

@ -22,6 +22,8 @@ import BinarySearchTrickPage from "./pages/BinarySearchTrickPage";
import BinarySearchTrickGreenScreen from "./pages/BinarySearchTrickGreenScreen"; import BinarySearchTrickGreenScreen from "./pages/BinarySearchTrickGreenScreen";
import GameOfSimGreenScreen from "./pages/GameOfSimGreenScreen"; import GameOfSimGreenScreen from "./pages/GameOfSimGreenScreen";
import GameOfSimPage from "./pages/GameOfSimPage"; 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 Foundations from "./pages/theme-pages/discrete-math/Foundations";
import Proofs from "./pages/theme-pages/discrete-math/Proofs"; import Proofs from "./pages/theme-pages/discrete-math/Proofs";
import Counting from "./pages/theme-pages/discrete-math/Counting"; 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="/binary-search-trick/:mode" element={<BinarySearchTrickGreenScreen />} />
<Route path="/game-of-sim" element={<GameOfSimPage />} /> <Route path="/game-of-sim" element={<GameOfSimPage />} />
<Route path="/game-of-sim/:mode" element={<GameOfSimGreenScreen />} /> <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 */} {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />

View file

@ -3,38 +3,60 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Search } from 'lucide-react'; import { Search } from 'lucide-react';
import Layout from '@/components/Layout';
import BinarySearchTrick from '@/components/BinarySearchTrick'; import BinarySearchTrick from '@/components/BinarySearchTrick';
import TernarySearchTrick from '@/components/TernarySearchTrick';
interface Interactive { interface Interactive {
id: string; id: string;
title: string; title: string;
description: string; description: string;
tags: string[]; tags: string[];
component: React.ComponentType; component: React.ComponentType<{ showSocialShare?: boolean }>;
} }
const interactives: Interactive[] = [{
id: 'binary-search-trick', const interactives: Interactive[] = [
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!', id: 'binary-search-trick',
tags: ['binary', 'magic', 'numbers', 'trick', 'data-structures'], title: 'Binary Search Magic Trick',
component: BinarySearchTrick 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 InteractiveGallery = () => {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [selectedInteractive, setSelectedInteractive] = useState<Interactive | null>(null); 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()))); 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) { if (selectedInteractive) {
const InteractiveComponent = selectedInteractive.component; const InteractiveComponent = selectedInteractive.component;
return <div className="space-y-6"> return (
<div className="flex items-center gap-4"> <div className="min-h-screen bg-background">
<button onClick={() => setSelectedInteractive(null)} className="text-primary hover:text-primary/80 font-medium"> Back to Data Structures and Algorithms</button> <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>
</div>
<InteractiveComponent showSocialShare={true} />
</div> </div>
<div className="bg-background border rounded-lg p-6"> </div>
<InteractiveComponent /> );
</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"> <div className="space-y-4">
<h1 className="text-3xl font-bold text-foreground">Data Structures and Algorithms</h1> <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> <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"> {filteredInteractives.length === 0 && <div className="text-center py-12">
<p className="text-muted-foreground text-lg">No interactives found matching your search.</p> <p className="text-muted-foreground text-lg">No interactives found matching your search.</p>
</div>} </div>}
</div>; </div>
</div>
</Layout>
);
}; };
export default InteractiveGallery; export default InteractiveGallery;

View file

@ -45,6 +45,15 @@ const allInteractives: Interactive[] = [
path: '/game-of-sim', path: '/game-of-sim',
theme: 'Games', theme: 'Games',
dateAdded: '2024-03-10' 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'
} }
]; ];

View 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;

View 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;

View 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;

View file

@ -1,14 +1,7 @@
import Layout from "@/components/Layout";
import InteractiveGallery from "@/components/InteractiveGallery"; import InteractiveGallery from "@/components/InteractiveGallery";
const DataStructures = () => { const DataStructures = () => {
return ( return <InteractiveGallery />;
<Layout>
<div className="max-w-7xl mx-auto p-6">
<InteractiveGallery />
</div>
</Layout>
);
}; };
export default DataStructures; export default DataStructures;