543 lines
No EOL
21 KiB
TypeScript
543 lines
No EOL
21 KiB
TypeScript
import React, { useState, useMemo } from 'react';
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||
import { Input } from '@/components/ui/input';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||
import { Search, SortAsc, SortDesc } from 'lucide-react';
|
||
import Layout from '@/components/Layout';
|
||
|
||
interface Interactive {
|
||
id: string;
|
||
title: string;
|
||
description: string;
|
||
tags: string[];
|
||
path: string;
|
||
theme: string;
|
||
dateAdded?: string; // For recency sorting
|
||
}
|
||
|
||
// This would ideally be generated from the actual components, but for now we'll hardcode
|
||
const allInteractives: Interactive[] = [
|
||
{
|
||
id: 'binary-number-game',
|
||
title: 'Binary Number Representation',
|
||
description: 'Learn how numbers are represented in binary (base-2) format through an interactive guessing game.',
|
||
tags: ['binary', 'numbers', 'conversion', 'representation'],
|
||
path: '/binary-number-game',
|
||
theme: 'Discrete Math',
|
||
dateAdded: '2024-01-15'
|
||
},
|
||
{
|
||
id: 'ternary-number-game',
|
||
title: 'Ternary Number Representation',
|
||
description: 'Learn how numbers are represented in ternary (base-3) format by toggling between 0, 1, or 2 copies of powers of three.',
|
||
tags: ['ternary', 'numbers', 'conversion', 'representation', 'base-3'],
|
||
path: '/ternary-number-game',
|
||
theme: 'Discrete Math',
|
||
dateAdded: '2024-01-16'
|
||
},
|
||
{
|
||
id: 'balanced-ternary-game',
|
||
title: 'Balanced Ternary Representation',
|
||
description: 'Explore balanced ternary using digits T (-1), 0, and 1. Add or subtract powers of three to match the target number.',
|
||
tags: ['balanced-ternary', 'numbers', 'conversion', 'representation', 'base-3', 'signed-digits'],
|
||
path: '/balanced-ternary-game',
|
||
theme: 'Discrete Math',
|
||
dateAdded: '2024-01-17'
|
||
},
|
||
{
|
||
id: 'zeckendorf-game',
|
||
title: 'Zeckendorf Representation',
|
||
description: 'Learn how numbers are represented using Fibonacci numbers with the unique Zeckendorf representation (no consecutive Fibonacci numbers).',
|
||
tags: ['fibonacci', 'numbers', 'representation', 'zeckendorf', 'combinatorics'],
|
||
path: '/discrete-math/foundations/zeckendorf',
|
||
theme: 'Discrete Math',
|
||
dateAdded: '2024-12-22'
|
||
},
|
||
{
|
||
id: 'zeckendorf-search-trick',
|
||
title: 'Zeckendorf Search Magic Trick',
|
||
description: 'Experience the magic of Zeckendorf representation! Think of a number and watch as the cards reveal it through Fibonacci numbers.',
|
||
tags: ['fibonacci', 'magic', 'numbers', 'trick', 'zeckendorf', 'representation'],
|
||
path: '/discrete-math/foundations/zeckendorf-search',
|
||
theme: 'Data Structures and Algorithms',
|
||
dateAdded: '2024-12-22'
|
||
},
|
||
{
|
||
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'],
|
||
path: '/binary-search-trick',
|
||
theme: 'Data Structures',
|
||
dateAdded: '2024-02-20'
|
||
},
|
||
{
|
||
id: 'game-of-sim',
|
||
title: 'Game of Sim',
|
||
description: 'A strategic two-player game where you take turns coloring edges between vertices, trying to avoid creating a triangle of your own color.',
|
||
tags: ['strategy', 'graph-theory', 'two-player', 'logic', 'game-theory'],
|
||
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'
|
||
},
|
||
{
|
||
id: 'northcotts-game',
|
||
title: "Northcott's Game",
|
||
description: 'A strategic board game where players move their pieces to outmaneuver their opponent in a tactical race.',
|
||
tags: ['strategy', 'board-game', 'two-player', 'logic', 'game-theory'],
|
||
path: '/northcotts-game',
|
||
theme: 'Games',
|
||
dateAdded: '2024-03-15'
|
||
},
|
||
{
|
||
id: 'plate-swap',
|
||
title: 'The Plate Swap Puzzle',
|
||
description: 'Arrange plates around a circular table so each person has their own plate. A challenging permutation puzzle!',
|
||
tags: ['logic', 'permutation', 'strategy', 'puzzle', 'cycles'],
|
||
path: '/puzzles/plate-swap',
|
||
theme: 'Puzzles',
|
||
dateAdded: '2024-12-19'
|
||
},
|
||
{
|
||
id: 'chessboard-repaint',
|
||
title: 'Chessboard Repaint Puzzle',
|
||
description: 'Repaint rows, columns, or 2×2 squares to achieve exactly one black square. A puzzle about parity and operations!',
|
||
tags: ['logic', 'parity', 'strategy', 'puzzle', 'chessboard', 'operations'],
|
||
path: '/puzzles/chessboard-repaint',
|
||
theme: 'Puzzles',
|
||
dateAdded: '2024-12-19'
|
||
},
|
||
{
|
||
id: 'bulgarian-solitaire',
|
||
title: 'Bulgarian Solitaire',
|
||
description: 'A mathematical card game where you repeatedly redistribute cards into piles until reaching a stable configuration.',
|
||
tags: ['solitaire', 'card-game', 'mathematics', 'puzzle', 'redistribution', 'stable-configuration'],
|
||
path: '/puzzles/bulgarian-solitaire',
|
||
theme: 'Puzzles',
|
||
dateAdded: '2024-12-22'
|
||
},
|
||
{
|
||
id: 'subtraction-game',
|
||
title: 'The Subtraction Game',
|
||
description: 'A strategic two-player game where players take turns removing circles, with the last player to move winning.',
|
||
tags: ['strategy', 'two-player', 'game-theory', 'mathematical-games', 'nim-variant'],
|
||
path: '/subtraction-game',
|
||
theme: 'Games',
|
||
dateAdded: '2024-12-22'
|
||
},
|
||
{
|
||
id: 'knights-puzzle',
|
||
title: 'Knights Exchange Puzzle',
|
||
description: 'A classic chess puzzle where you must exchange the positions of white and black knights using legal knight moves.',
|
||
tags: ['chess', 'knights', 'puzzle', 'logic', 'strategy'],
|
||
path: '/knights-puzzle',
|
||
theme: 'Puzzles',
|
||
dateAdded: '2024-07-21'
|
||
},
|
||
{
|
||
id: 'gold-coin',
|
||
title: 'The Gold Coin Game',
|
||
description: 'A strategic two-player game where you move coins to the left or take the leftmost coin. The player who takes the gold coin wins!',
|
||
tags: ['strategy', 'two-player', 'logic', 'game-theory', 'movement', 'coins'],
|
||
path: '/games/gold-coin',
|
||
theme: 'Games',
|
||
dateAdded: '2024-12-19'
|
||
},
|
||
{
|
||
id: 'assisted-nim',
|
||
title: 'Assisted Game of Nim',
|
||
description: 'Learn the XOR strategy! Visualize how powers of 2 determine winning moves with color-coded borders.',
|
||
tags: ['strategy', 'two-player', 'math', 'logic', 'game-theory', 'xor-strategy', 'educational'],
|
||
path: '/games/assisted-nim',
|
||
theme: 'Games',
|
||
dateAdded: '2024-07-21'
|
||
},
|
||
{
|
||
id: 'nim',
|
||
title: 'Game of Nim',
|
||
description: 'A classic two-player game of strategy. Take turns removing stones from heaps. The player to take the last stone wins!',
|
||
tags: ['strategy', 'two-player', 'math', 'logic', 'game-theory'],
|
||
path: '/games/nim',
|
||
theme: 'Games',
|
||
dateAdded: '2024-07-21'
|
||
},
|
||
{
|
||
id: 'grid-tiling-puzzle',
|
||
title: 'Grid Tiling Puzzle',
|
||
description: 'Based on IMO 2025: Place rectangular tiles on a grid so that each row and column has exactly one uncovered square.',
|
||
tags: ['puzzle', 'geometry', 'optimization', 'imo', 'contest-problems'],
|
||
path: '/contest-problems/grid-tiling',
|
||
theme: 'Contest Problems',
|
||
dateAdded: '2024-12-22'
|
||
},
|
||
{
|
||
id: 'sunny-lines-puzzle',
|
||
title: 'Sunny Lines Puzzle',
|
||
description: 'Based on IMO 2025 P6: Place lines to cover points in a triangle. A line is "sunny" if it\'s not parallel to x-axis, y-axis, or x+y=0.',
|
||
tags: ['puzzle', 'geometry', 'lines', 'imo', 'contest-problems'],
|
||
path: '/contest-problems/sunny-lines',
|
||
theme: 'Contest Problems',
|
||
dateAdded: '2024-12-22'
|
||
},
|
||
{
|
||
id: 'sikinia-parliament',
|
||
title: 'Parliament of Sikinia Puzzle',
|
||
description: 'Separate parliament members into two houses so each has at most one enemy in their house. A graph theory puzzle!',
|
||
tags: ['graph-theory', 'logic', 'coloring'],
|
||
path: '/puzzles/sikinia-parliament',
|
||
theme: 'Puzzles',
|
||
dateAdded: '2024-12-22'
|
||
},
|
||
{
|
||
id: 'guessing-game',
|
||
title: 'Number Guessing Game',
|
||
description: 'Experience different search algorithms through interactive gameplay. Compare random, linear, and binary search strategies!',
|
||
tags: ['algorithms', 'search', 'educational', 'interactive', 'binary-search', 'computer-science'],
|
||
path: '/games/guessing',
|
||
theme: 'Data Structures and Algorithms',
|
||
dateAdded: '2024-12-23'
|
||
},
|
||
{
|
||
id: 'n-queens',
|
||
title: 'N-Queens Puzzle',
|
||
description: 'Place as many queens as possible on an n×n chessboard so that no two queens attack each other.',
|
||
tags: ['logic', 'chess', 'placement', 'classic'],
|
||
path: '/puzzles/n-queens',
|
||
theme: 'Puzzles',
|
||
dateAdded: '2024-12-22'
|
||
},
|
||
{
|
||
id: 'rules-of-inference',
|
||
title: 'Rules of Inference Playground',
|
||
description: 'Practice applying resolution and equivalence rules to derive conclusions step by step in this interactive logic playground.',
|
||
tags: ['logic', 'inference', 'playground', 'rules', 'proofs', 'educational'],
|
||
path: '/discrete-math/foundations/rules-of-inference',
|
||
theme: 'Discrete Math',
|
||
dateAdded: '2024-12-23'
|
||
},
|
||
{
|
||
id: 'stacking-blocks',
|
||
title: 'Stacking Blocks Optimization',
|
||
description: 'Split stacks of blocks to maximize your score! An optimization puzzle about strategic decision-making.',
|
||
tags: ['optimization', 'strategy', 'mathematical', 'algorithm', 'puzzles'],
|
||
path: '/puzzles/stacking-blocks',
|
||
theme: 'Puzzles',
|
||
dateAdded: '2025-01-22'
|
||
},
|
||
{
|
||
id: 'craps-game',
|
||
title: 'Craps: An Exploration',
|
||
description: 'Experience the classic casino dice game! Roll two dice and learn about probability as you discover the rules of the opening throw.',
|
||
tags: ['probability', 'dice', 'casino', 'mathematics', 'statistics', 'gambling'],
|
||
path: '/themes/games/craps-game',
|
||
theme: 'Games',
|
||
dateAdded: '2025-01-05'
|
||
},
|
||
{
|
||
id: 'neighbor-sum-avoidance',
|
||
title: 'Neighbor Sum Avoidance',
|
||
description: 'Arrange numbers in a circle so that the sum of two neighbors is never divisible by 3, 5, or 7. An interactive graph theory puzzle!',
|
||
tags: ['graph-theory', 'puzzle', 'arrangement', 'divisibility', 'circle', 'mathematics'],
|
||
path: '/themes/discrete-math/graphs/neighbor-sum-avoidance',
|
||
theme: 'Discrete Math',
|
||
dateAdded: '2025-01-22'
|
||
},
|
||
{
|
||
id: 'burnsides-lemma',
|
||
title: "Burnside's Lemma",
|
||
description: 'Explore how to count distinct objects under symmetry by creating necklaces with different bead and color combinations.',
|
||
tags: ['group-theory', 'symmetry', 'combinatorics', 'counting', 'algebra', 'structures'],
|
||
path: '/themes/discrete-math/structures/burnsides-lemma',
|
||
theme: 'Discrete Math',
|
||
dateAdded: '2025-01-22'
|
||
},
|
||
{
|
||
id: 'cube-coloring',
|
||
title: "Burnside's Lemma: Cube Coloring",
|
||
description: 'Explore cube rotations and their correspondence with permutations. Paint a 3D cube and understand the rotational symmetry group.',
|
||
tags: ['group-theory', 'symmetry', 'cube', 'rotations', 'permutations', 'algebra', '3d'],
|
||
path: '/themes/discrete-math/structures/cube-coloring',
|
||
theme: 'Discrete Math',
|
||
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'
|
||
},
|
||
{
|
||
id: 'ladybug-clock',
|
||
title: 'The Ladybug Clock Puzzle',
|
||
description: 'A ladybug walks randomly on a clock face. Discover the surprising probability that any given number is painted last!',
|
||
tags: ['probability', 'random-walk', 'simulation', 'statistics', 'cover-time', 'puzzle'],
|
||
path: '/puzzles/ladybug-clock',
|
||
theme: 'Puzzles',
|
||
dateAdded: '2025-01-21'
|
||
}
|
||
];
|
||
|
||
type SortField = 'title' | 'dateAdded';
|
||
type SortDirection = 'asc' | 'desc';
|
||
|
||
const InteractiveIndex = () => {
|
||
const [searchTerm, setSearchTerm] = useState('');
|
||
const [selectedTag, setSelectedTag] = useState<string>('');
|
||
const [sortField, setSortField] = useState<SortField>('title');
|
||
const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
|
||
const [currentPage, setCurrentPage] = useState(1);
|
||
const itemsPerPage = 10;
|
||
const [showTags, setShowTags] = useState(false);
|
||
|
||
// Get all unique tags
|
||
const allTags = useMemo(() => {
|
||
const tags = new Set<string>();
|
||
allInteractives.forEach(interactive => {
|
||
interactive.tags.forEach(tag => tags.add(tag));
|
||
});
|
||
return Array.from(tags).sort();
|
||
}, []);
|
||
|
||
// Filter and sort interactives
|
||
const filteredAndSortedInteractives = useMemo(() => {
|
||
let 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()));
|
||
|
||
const matchesTag = !selectedTag || interactive.tags.includes(selectedTag);
|
||
|
||
return matchesSearch && matchesTag;
|
||
});
|
||
|
||
// Sort
|
||
filtered.sort((a, b) => {
|
||
let aValue = a[sortField];
|
||
let bValue = b[sortField];
|
||
|
||
if (sortField === 'title') {
|
||
aValue = aValue.toLowerCase();
|
||
bValue = bValue.toLowerCase();
|
||
}
|
||
|
||
if (aValue < bValue) return sortDirection === 'asc' ? -1 : 1;
|
||
if (aValue > bValue) return sortDirection === 'asc' ? 1 : -1;
|
||
return 0;
|
||
});
|
||
|
||
return filtered;
|
||
}, [searchTerm, selectedTag, sortField, sortDirection]);
|
||
|
||
// Pagination
|
||
const totalPages = Math.ceil(filteredAndSortedInteractives.length / itemsPerPage);
|
||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||
const paginatedInteractives = filteredAndSortedInteractives.slice(startIndex, startIndex + itemsPerPage);
|
||
|
||
const handleTagClick = (tag: string) => {
|
||
setSelectedTag(selectedTag === tag ? '' : tag);
|
||
setCurrentPage(1);
|
||
};
|
||
|
||
const handleSortChange = (field: SortField) => {
|
||
if (sortField === field) {
|
||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||
} else {
|
||
setSortField(field);
|
||
setSortDirection('asc');
|
||
}
|
||
setCurrentPage(1);
|
||
};
|
||
|
||
return (
|
||
<Layout>
|
||
<div className="py-12 px-4">
|
||
<div className="max-w-6xl mx-auto">
|
||
<h1 className="text-4xl font-bold text-foreground mb-8">All Interactives ({allInteractives.length})</h1>
|
||
|
||
{/* Search and Sort Controls */}
|
||
<div className="flex flex-col sm:flex-row gap-4 mb-8">
|
||
<div className="relative flex-1">
|
||
<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);
|
||
setCurrentPage(1);
|
||
}}
|
||
className="pl-10"
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex gap-2">
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => handleSortChange('title')}
|
||
className="flex items-center gap-2"
|
||
>
|
||
{sortField === 'title' ? (
|
||
sortDirection === 'asc' ? <SortAsc className="w-4 h-4" /> : <SortDesc className="w-4 h-4" />
|
||
) : (
|
||
<SortAsc className="w-4 h-4" />
|
||
)}
|
||
Title
|
||
</Button>
|
||
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => handleSortChange('dateAdded')}
|
||
className="flex items-center gap-2"
|
||
>
|
||
{sortField === 'dateAdded' ? (
|
||
sortDirection === 'asc' ? <SortAsc className="w-4 h-4" /> : <SortDesc className="w-4 h-4" />
|
||
) : (
|
||
<SortAsc className="w-4 h-4" />
|
||
)}
|
||
Recency
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Tag Filter */}
|
||
{selectedTag && (
|
||
<div className="mb-6">
|
||
<span className="text-sm text-muted-foreground mr-2">Filtered by:</span>
|
||
<Badge
|
||
variant="secondary"
|
||
className="cursor-pointer hover:bg-destructive hover:text-destructive-foreground"
|
||
onClick={() => setSelectedTag('')}
|
||
>
|
||
{selectedTag} ×
|
||
</Badge>
|
||
</div>
|
||
)}
|
||
|
||
{/* Tags */}
|
||
<div className="mb-8">
|
||
<div className="flex items-center mb-3">
|
||
<h3 className="text-lg font-semibold text-foreground mr-3">Filter by Tags</h3>
|
||
<Button variant="ghost" size="sm" onClick={() => setShowTags(v => !v)}>
|
||
{showTags ? 'Hide Tags' : 'Show all Tags'}
|
||
</Button>
|
||
</div>
|
||
{showTags && (
|
||
<div className="flex flex-wrap gap-2">
|
||
{allTags.map(tag => (
|
||
<Badge
|
||
key={tag}
|
||
variant={selectedTag === tag ? "default" : "outline"}
|
||
className="cursor-pointer hover:bg-primary hover:text-primary-foreground"
|
||
onClick={() => handleTagClick(tag)}
|
||
>
|
||
{tag}
|
||
</Badge>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Results Count */}
|
||
<div className="mb-6">
|
||
<p className="text-muted-foreground">
|
||
Showing {filteredAndSortedInteractives.length} of {allInteractives.length} interactives
|
||
</p>
|
||
</div>
|
||
|
||
{/* Interactive List */}
|
||
<div className="space-y-4 mb-8">
|
||
{paginatedInteractives.map(interactive => (
|
||
<Card key={interactive.id} className="hover:shadow-md transition-shadow">
|
||
<CardHeader>
|
||
<CardTitle className="text-xl">
|
||
<a
|
||
href={interactive.path}
|
||
className="text-foreground hover:text-primary transition-colors"
|
||
>
|
||
{interactive.title}
|
||
</a>
|
||
</CardTitle>
|
||
<div className="flex flex-wrap gap-2 mt-2">
|
||
{interactive.tags.map(tag => (
|
||
<Badge
|
||
key={tag}
|
||
variant="secondary"
|
||
className="cursor-pointer hover:bg-primary hover:text-primary-foreground"
|
||
onClick={() => handleTagClick(tag)}
|
||
>
|
||
{tag}
|
||
</Badge>
|
||
))}
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<p className="text-muted-foreground">{interactive.description}</p>
|
||
<div className="mt-3">
|
||
<Badge variant="outline" className="text-xs">
|
||
{interactive.theme}
|
||
</Badge>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
|
||
{/* Pagination */}
|
||
{totalPages > 1 && (
|
||
<div className="flex justify-center gap-2">
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
|
||
disabled={currentPage === 1}
|
||
>
|
||
Previous
|
||
</Button>
|
||
|
||
<div className="flex items-center gap-2">
|
||
{Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
|
||
<Button
|
||
key={page}
|
||
variant={currentPage === page ? "default" : "outline"}
|
||
size="sm"
|
||
onClick={() => setCurrentPage(page)}
|
||
>
|
||
{page}
|
||
</Button>
|
||
))}
|
||
</div>
|
||
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
|
||
disabled={currentPage === totalPages}
|
||
>
|
||
Next
|
||
</Button>
|
||
</div>
|
||
)}
|
||
|
||
{paginatedInteractives.length === 0 && (
|
||
<div className="text-center py-12">
|
||
<p className="text-muted-foreground text-lg">No interactives found matching your criteria.</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</Layout>
|
||
);
|
||
};
|
||
|
||
export default InteractiveIndex;
|