interactives/src/components/InteractiveIndex.tsx
Neeldhara Misra 7451682dd5 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
2025-07-20 10:29:09 +05:30

301 lines
No EOL
11 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: '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'
}
];
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;
// 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</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">
<h3 className="text-lg font-semibold text-foreground mb-3">Filter by Tags</h3>
<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;