interactives/src/components/InteractiveIndex.tsx
gpt-engineer-app[bot] d050cbe520 Add N-Queens puzzle
Create an N-Queens puzzle where users can place queens on an n x n chessboard. The size of the board can be adjusted using a slider. A timer can be started before placing any queens. Clicking a square places a queen, and clicking a queen removes it. Attacked squares are visually indicated.
2025-07-24 10:36:29 +00:00

444 lines
No EOL
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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: '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: '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'
}
];
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;