Standardize page wrappers, add showSocialShare prop, and add Cmd+K search

- Standardize all page wrappers to use consistent min-h-screen bg-background pattern
- Remove Layout wrapper usage from pages that had it
- Remove page-level SocialShare and headers, move into components
- Add showSocialShare prop to all interactive components for consistent control
- Add Cmd+K / Ctrl+K command palette search across all interactives
- Export allInteractives from InteractiveIndex for reuse

Co-Authored-By: Neeldhara Misra <mail@neeldhara.com>
This commit is contained in:
Devin AI 2026-02-18 20:07:30 +00:00
parent dc8300f358
commit 8f2a086df6
35 changed files with 259 additions and 357 deletions

View file

@ -3,6 +3,7 @@ import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import ErrorBoundary from "@/components/ErrorBoundary"; import ErrorBoundary from "@/components/ErrorBoundary";
import CommandSearch from "@/components/CommandSearch";
import { BrowserRouter, Routes, Route } from "react-router-dom"; import { BrowserRouter, Routes, Route } from "react-router-dom";
import Index from "./pages/Index"; import Index from "./pages/Index";
import InteractiveIndex from "./components/InteractiveIndex"; import InteractiveIndex from "./components/InteractiveIndex";
@ -78,6 +79,7 @@ const App = () => (
<Toaster /> <Toaster />
<Sonner /> <Sonner />
<BrowserRouter> <BrowserRouter>
<CommandSearch />
<Routes> <Routes>
<Route path="/" element={<Index />} /> <Route path="/" element={<Index />} />
<Route path="/index" element={<InteractiveIndex />} /> <Route path="/index" element={<InteractiveIndex />} />

View file

@ -66,7 +66,11 @@ const getOddPowers = (heaps: number[]): Set<number> => {
return oddPowers; return oddPowers;
}; };
const AssistedNimGame: React.FC = () => { interface AssistedNimGameProps {
showSocialShare?: boolean;
}
const AssistedNimGame: React.FC<AssistedNimGameProps> = ({ showSocialShare = true }) => {
const [mode, setMode] = useState<Mode | null>(null); const [mode, setMode] = useState<Mode | null>(null);
const [showSetup, setShowSetup] = useState(false); const [showSetup, setShowSetup] = useState(false);
const [userHeapCount, setUserHeapCount] = useState(3); const [userHeapCount, setUserHeapCount] = useState(3);
@ -384,12 +388,12 @@ const AssistedNimGame: React.FC = () => {
</CardContent> </CardContent>
</Card> </Card>
{/* Social Share */} {showSocialShare && (
<SocialShare <SocialShare
title="Assisted Game of Nim - XOR Strategy Visualization" title="Assisted Game of Nim - XOR Strategy Visualization"
description="Learn the mathematical strategy behind Nim with visual power-of-2 breakdown!" description="Learn the mathematical strategy behind Nim with visual power-of-2 breakdown!"
url={window.location.href}
/> />
)}
</div> </div>
); );
}; };

View file

@ -0,0 +1,65 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { allInteractives } from "@/components/InteractiveIndex";
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { Badge } from "@/components/ui/badge";
const CommandSearch = () => {
const [open, setOpen] = useState(false);
const navigate = useNavigate();
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((prev) => !prev);
}
};
document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down);
}, []);
const handleSelect = (path: string) => {
setOpen(false);
navigate(path);
};
return (
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Search interactives by title or description..." />
<CommandList>
<CommandEmpty>No interactives found.</CommandEmpty>
<CommandGroup heading="Interactives">
{allInteractives.map((interactive) => (
<CommandItem
key={interactive.id}
value={`${interactive.title} ${interactive.description}`}
onSelect={() => handleSelect(interactive.path)}
>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span className="font-medium">{interactive.title}</span>
<Badge variant="outline" className="text-xs">
{interactive.theme}
</Badge>
</div>
<span className="text-xs text-muted-foreground line-clamp-1">
{interactive.description}
</span>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</CommandDialog>
);
};
export default CommandSearch;

View file

@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Shield, Target, RotateCcw, Info } from "lucide-react"; import { Shield, Target, RotateCcw, Info } from "lucide-react";
import SocialShare from '@/components/SocialShare';
// Extended good rectangle: 10 columns × 12 rows (following Fig. 10) // Extended good rectangle: 10 columns × 12 rows (following Fig. 10)
// Border: columns 0 and 9, rows 0/1 (top) and 10/11 (bottom) // Border: columns 0 and 9, rows 0/1 (top) and 10/11 (bottom)
@ -349,7 +350,11 @@ const defendAttack = (
}; };
const EternalDominationGame: React.FC = () => { interface EternalDominationGameProps {
showSocialShare?: boolean;
}
const EternalDominationGame: React.FC<EternalDominationGameProps> = ({ showSocialShare = true }) => {
const [guards, setGuards] = useState<Set<string>>(generateInitialGuards); const [guards, setGuards] = useState<Set<string>>(generateInitialGuards);
const [attackHistory, setAttackHistory] = useState<Position[]>([]); const [attackHistory, setAttackHistory] = useState<Position[]>([]);
const [message, setMessage] = useState<string>("Click any unguarded cell to attack."); const [message, setMessage] = useState<string>("Click any unguarded cell to attack.");
@ -679,6 +684,13 @@ const EternalDominationGame: React.FC = () => {
)} )}
</CardContent> </CardContent>
</Card> </Card>
{showSocialShare && (
<SocialShare
title="Eternal Domination on a Grid"
description="Explore the m-eternal domination problem - can you attack in a way that breaks the defense?"
/>
)}
</div> </div>
); );
}; };

View file

@ -4,8 +4,13 @@ import { Button } from "@/components/ui/button";
import { Slider } from "@/components/ui/slider"; import { Slider } from "@/components/ui/slider";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { toast } from "sonner"; import { toast } from "sonner";
import SocialShare from '@/components/SocialShare';
const FerrersRogersRamanujan = () => { interface FerrersRogersRamanujanProps {
showSocialShare?: boolean;
}
const FerrersRogersRamanujan = ({ showSocialShare = true }: FerrersRogersRamanujanProps) => {
const [n, setN] = useState(14); const [n, setN] = useState(14);
const [partitionSliders, setPartitionSliders] = useState<number[]>([]); const [partitionSliders, setPartitionSliders] = useState<number[]>([]);
const [committedPartition, setCommittedPartition] = useState<number[]>([]); const [committedPartition, setCommittedPartition] = useState<number[]>([]);
@ -330,6 +335,13 @@ const FerrersRogersRamanujan = () => {
The hook sizes form a new partition where parts differ by at least 2. The hook sizes form a new partition where parts differ by at least 2.
</p> </p>
</div> </div>
{showSocialShare && (
<SocialShare
title="Ferrers Diagram & Rogers-Ramanujan Partition"
description="Explore integer partitions through visual Ferrers diagrams and discover the elegant Rogers-Ramanujan transformation."
/>
)}
</div> </div>
</Card> </Card>
); );

View file

@ -18,7 +18,11 @@ interface GameState {
moveHistory: string[]; moveHistory: string[];
} }
const GoldCoinGame: React.FC = () => { interface GoldCoinGameProps {
showSocialShare?: boolean;
}
const GoldCoinGame: React.FC<GoldCoinGameProps> = ({ showSocialShare = true }) => {
const [gameState, setGameState] = useState<GameState>({ const [gameState, setGameState] = useState<GameState>({
track: Array(15).fill('empty'), track: Array(15).fill('empty'),
currentPlayer: 0, currentPlayer: 0,
@ -421,11 +425,12 @@ const GoldCoinGame: React.FC = () => {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{showSocialShare && (
<SocialShare <SocialShare
title="The Gold Coin Game" title="The Gold Coin Game"
description="A strategic two-player game where you move coins to acquire the gold coin!" description="A strategic two-player game where you move coins to acquire the gold coin!"
url={window.location.href}
/> />
)}
</div> </div>
); );
}; };

View file

@ -18,7 +18,7 @@ interface Interactive {
} }
// This would ideally be generated from the actual components, but for now we'll hardcode // This would ideally be generated from the actual components, but for now we'll hardcode
const allInteractives: Interactive[] = [ export const allInteractives: Interactive[] = [
{ {
id: 'binary-number-game', id: 'binary-number-game',
title: 'Binary Number Representation', title: 'Binary Number Representation',

View file

@ -27,7 +27,11 @@ const heapColors = [
'#f87171', '#a78bfa', '#fbbf24', '#60a5fa', '#34d399', '#f472b6', '#f87171', '#a78bfa', '#facc15', '#38bdf8', '#f87171', '#a78bfa', '#fbbf24', '#60a5fa', '#34d399', '#f472b6', '#f87171', '#a78bfa', '#facc15', '#38bdf8',
]; ];
const NimGame: React.FC = () => { interface NimGameProps {
showSocialShare?: boolean;
}
const NimGame: React.FC<NimGameProps> = ({ showSocialShare = true }) => {
const [mode, setMode] = useState<Mode | null>(null); const [mode, setMode] = useState<Mode | null>(null);
const [showSetup, setShowSetup] = useState(false); const [showSetup, setShowSetup] = useState(false);
const [userHeapCount, setUserHeapCount] = useState(3); const [userHeapCount, setUserHeapCount] = useState(3);
@ -275,14 +279,12 @@ const NimGame: React.FC = () => {
</CardContent> </CardContent>
</Card> </Card>
{/* Social Share */} {showSocialShare && (
<div className="flex justify-center">
<SocialShare <SocialShare
title="Game of Nim" title="Game of Nim"
description="Play the classic two-player game of Nim! Take turns removing stones from heaps. The player to take the last stone wins!" description="Play the classic two-player game of Nim! Take turns removing stones from heaps. The player to take the last stone wins!"
url={`${window.location.origin}/games/nim`}
/> />
</div> )}
</div> </div>
); );
}; };

View file

@ -11,6 +11,7 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { RefreshCw, User, UserRound, Users, UserCheck } from "lucide-react"; import { RefreshCw, User, UserRound, Users, UserCheck } from "lucide-react";
import SocialShare from '@/components/SocialShare';
const AGENTS = ["Alice", "Bob", "Charlie", "Dana"]; const AGENTS = ["Alice", "Bob", "Charlie", "Dana"];
const AGENT_ICONS = [User, UserRound, Users, UserCheck]; const AGENT_ICONS = [User, UserRound, Users, UserCheck];
@ -23,9 +24,10 @@ const ROOM_COLORS = [
interface RentDivisionPuzzleProps { interface RentDivisionPuzzleProps {
onComplete?: (completed: boolean) => void; onComplete?: (completed: boolean) => void;
showSocialShare?: boolean;
} }
const RentDivisionPuzzle = ({ onComplete }: RentDivisionPuzzleProps) => { const RentDivisionPuzzle = ({ onComplete, showSocialShare = true }: RentDivisionPuzzleProps) => {
// V-matrix: valuations[agent][room] // V-matrix: valuations[agent][room]
const [valuations, setValuations] = useState<number[][]>(() => const [valuations, setValuations] = useState<number[][]>(() =>
Array.from({ length: 4 }, () => Array.from({ length: 4 }, () =>
@ -469,6 +471,12 @@ const RentDivisionPuzzle = ({ onComplete }: RentDivisionPuzzleProps) => {
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
{showSocialShare && (
<SocialShare
title="Envy-Free Rent Division"
description="Explore fair division by assigning rooms and setting rents to achieve envy-free allocations."
/>
)}
</div> </div>
); );
}; };

View file

@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import SocialShare from '@/components/SocialShare';
// Rules of Inference Playground (Example 1: Resolution) // Rules of Inference Playground (Example 1: Resolution)
// Layout: 3 columns (Premises | Notepad lines with rule margin | Rules) // Layout: 3 columns (Premises | Notepad lines with rule margin | Rules)
@ -196,7 +197,7 @@ function DropZone({ children, onDrop, className }: { children?: React.ReactNode;
); );
} }
export default function RulesOfInferencePlayground({ onComplete }: { onComplete?: (completed: boolean) => void }) { export default function RulesOfInferencePlayground({ onComplete, showSocialShare = true }: { onComplete?: (completed: boolean) => void; showSocialShare?: boolean }) {
const { toast } = useToast(); const { toast } = useToast();
const [exampleId, setExampleId] = useState(EXAMPLES[0].id); const [exampleId, setExampleId] = useState(EXAMPLES[0].id);
const example = useMemo(() => EXAMPLES.find((e) => e.id === exampleId)!, [exampleId]); const example = useMemo(() => EXAMPLES.find((e) => e.id === exampleId)!, [exampleId]);
@ -579,6 +580,13 @@ export default function RulesOfInferencePlayground({ onComplete }: { onComplete?
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
{showSocialShare && (
<SocialShare
title="Rules of Inference Playground"
description="Practice applying resolution and equivalence rules to derive conclusions step by step in this interactive logic playground."
/>
)}
</div> </div>
); );
} }

View file

@ -1,13 +1,12 @@
import Layout from "@/components/Layout";
import AscDescGridPuzzle from "@/components/AscDescGridPuzzle"; import AscDescGridPuzzle from "@/components/AscDescGridPuzzle";
const AscDescGridPuzzlePage = () => { const AscDescGridPuzzlePage = () => {
return ( return (
<Layout> <div className="min-h-screen bg-background">
<div className="py-8"> <div className="container mx-auto px-4 py-8">
<AscDescGridPuzzle /> <AscDescGridPuzzle showSocialShare={true} />
</div>
</div> </div>
</Layout>
); );
}; };

View file

@ -1,10 +1,11 @@
import React from 'react';
import AssistedNimGame from '@/components/AssistedNimGame'; import AssistedNimGame from '@/components/AssistedNimGame';
const AssistedNimGamePage = () => { const AssistedNimGamePage = () => {
return ( return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<AssistedNimGame /> <AssistedNimGame showSocialShare={true} />
</div>
</div> </div>
); );
}; };

View file

@ -1,13 +1,12 @@
import Layout from "@/components/Layout";
import BagchalGame from "@/components/BagchalGame"; import BagchalGame from "@/components/BagchalGame";
const BagchalGamePage = () => { const BagchalGamePage = () => {
return ( return (
<Layout> <div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<BagchalGame showSocialShare={true} /> <BagchalGame showSocialShare={true} />
</div> </div>
</Layout> </div>
); );
}; };

View file

@ -2,9 +2,9 @@ import BalancedTernaryGame from '@/components/BalancedTernaryGame';
const BalancedTernaryGamePage = () => { const BalancedTernaryGamePage = () => {
return ( return (
<div className="min-h-screen bg-background p-6"> <div className="min-h-screen bg-background">
<div className="max-w-4xl mx-auto"> <div className="container mx-auto px-4 py-8">
<BalancedTernaryGame /> <BalancedTernaryGame showSocialShare={true} />
</div> </div>
</div> </div>
); );

View file

@ -2,9 +2,9 @@ import BinaryNumberGame from '@/components/BinaryNumberGame';
const BinaryNumberGamePage = () => { const BinaryNumberGamePage = () => {
return ( return (
<div className="min-h-screen bg-background p-6"> <div className="min-h-screen bg-background">
<div className="max-w-4xl mx-auto"> <div className="container mx-auto px-4 py-8">
<BinaryNumberGame /> <BinaryNumberGame showSocialShare={true} />
</div> </div>
</div> </div>
); );

View file

@ -1,29 +1,13 @@
import BulgarianSolitaire from "@/components/BulgarianSolitaire"; import BulgarianSolitaire from "@/components/BulgarianSolitaire";
import { useSearchParams } from "react-router-dom";
const BulgarianSolitairePage = () => { const BulgarianSolitairePage = () => {
const [searchParams] = useSearchParams();
const from = searchParams.get('from');
if (from === 'puzzles') {
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8 space-y-6"> <div className="container mx-auto px-4 py-8">
<div className="flex items-center gap-4">
<button
onClick={() => window.location.href = '/themes/puzzles'}
className="text-primary hover:text-primary/80 font-medium"
>
Back to Puzzles
</button>
</div>
<BulgarianSolitaire showSocialShare={true} /> <BulgarianSolitaire showSocialShare={true} />
</div> </div>
</div> </div>
); );
}
return <BulgarianSolitaire showSocialShare={true} />;
}; };
export default BulgarianSolitairePage; export default BulgarianSolitairePage;

View file

@ -1,13 +1,12 @@
import Layout from '@/components/Layout';
import BurnsidesLemma from '@/components/BurnsidesLemma'; import BurnsidesLemma from '@/components/BurnsidesLemma';
const BurnsidesLemmaPage = () => { const BurnsidesLemmaPage = () => {
return ( return (
<Layout> <div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<BurnsidesLemma showSocialShare={true} /> <BurnsidesLemma showSocialShare={true} />
</div> </div>
</Layout> </div>
); );
}; };

View file

@ -1,16 +1,12 @@
import Layout from '@/components/Layout';
import CubeColoring from '@/components/CubeColoring'; import CubeColoring from '@/components/CubeColoring';
const CubeColoringPage = () => { const CubeColoringPage = () => {
return ( return (
<Layout> <div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<CubeColoring <CubeColoring showSocialShare={true} />
showSocialShare={true} </div>
shareUrl={window.location.href}
/>
</div> </div>
</Layout>
); );
}; };

View file

@ -1,27 +1,12 @@
import Layout from '@/components/Layout';
import DominoRetilingPuzzle from '@/components/DominoRetilingPuzzle'; import DominoRetilingPuzzle from '@/components/DominoRetilingPuzzle';
import { useLocation, Link } from 'react-router-dom';
import { ArrowLeft } from 'lucide-react';
const DominoRetilingPuzzlePage = () => { const DominoRetilingPuzzlePage = () => {
const location = useLocation();
const fromPuzzles = location.search.includes('from=puzzles');
return ( return (
<Layout> <div className="min-h-screen bg-background">
{fromPuzzles && ( <div className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto px-6 pt-4"> <DominoRetilingPuzzle showSocialShare={true} />
<Link </div>
to="/themes/puzzles"
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<ArrowLeft className="w-4 h-4 mr-1" />
Back to Puzzles
</Link>
</div> </div>
)}
<DominoRetilingPuzzle />
</Layout>
); );
}; };

View file

@ -1,15 +1,12 @@
import Layout from "@/components/Layout";
import ErdosDiscrepancyPuzzle from "@/components/ErdosDiscrepancyPuzzle"; import ErdosDiscrepancyPuzzle from "@/components/ErdosDiscrepancyPuzzle";
const ErdosDiscrepancyPuzzlePage = () => { const ErdosDiscrepancyPuzzlePage = () => {
return ( return (
<Layout> <div className="min-h-screen bg-background">
<div className="py-8 px-4"> <div className="container mx-auto px-4 py-8">
<div className="max-w-6xl mx-auto">
<ErdosDiscrepancyPuzzle showSocialShare={true} /> <ErdosDiscrepancyPuzzle showSocialShare={true} />
</div> </div>
</div> </div>
</Layout>
); );
}; };

View file

@ -1,87 +1,12 @@
import Layout from "@/components/Layout";
import EternalDominationGame from "@/components/EternalDominationGame"; import EternalDominationGame from "@/components/EternalDominationGame";
import SocialShare from "@/components/SocialShare";
import { Badge } from "@/components/ui/badge";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
const EternalDominationGamePage = () => { const EternalDominationGamePage = () => {
return ( return (
<Layout> <div className="min-h-screen bg-background">
<div className="py-8 px-4"> <div className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto space-y-6"> <EternalDominationGame showSocialShare={true} />
<div className="text-center space-y-4">
<Badge variant="outline" className="mb-2">Miscellany</Badge>
<h1 className="text-3xl font-bold">Eternal Domination on a Grid</h1>
<p className="text-muted-foreground max-w-2xl mx-auto">
Explore the m-eternal domination problem on a 12×10 grid. Guards must maintain
a dominating set while defending against arbitrary attack sequences.
</p>
</div>
<EternalDominationGame />
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="background">
<AccordionTrigger>Mathematical Background</AccordionTrigger>
<AccordionContent className="space-y-3 text-sm">
<p>
<strong>Dominating Set:</strong> A subset S of vertices in a graph G such that
every vertex not in S has a neighbor in S. Think of guards placed at certain
vertices that can "watch" their neighbors.
</p>
<p>
<strong>m-Eternal Domination:</strong> A two-player game where the defender
places guards on vertices, and the attacker repeatedly attacks unguarded vertices.
The defender responds by moving guards (all can move simultaneously, but only to
adjacent vertices) such that one guard ends on the attacked vertex and the guards
still form a dominating set.
</p>
<p>
<strong>The Challenge:</strong> The defender wins if they can maintain a dominating
set forever against any attack sequence. The m-eternal domination number γ(G) is
the minimum number of guards needed to win.
</p>
</AccordionContent>
</AccordionItem>
<AccordionItem value="strategy">
<AccordionTrigger>Defense Strategy</AccordionTrigger>
<AccordionContent className="space-y-3 text-sm">
<p>
This implementation uses the strategy from research on finite grids:
</p>
<ul className="list-disc pl-5 space-y-1">
<li><strong>Border guards:</strong> All vertices on the border (rows 0, 1, 8, 9 and
columns 0, 11) are always guarded, forming a protective cycle C.</li>
<li><strong>Interior guards:</strong> Placed in a pattern that ensures every interior
vertex is dominated by exactly one guard.</li>
<li><strong>Defense mechanism:</strong> When attacked, interior guards shift toward
the attack, potentially pushing a guard to the border. Border guards shift along
"complementary paths" to fill the resulting gaps.</li>
</ul>
</AccordionContent>
</AccordionItem>
<AccordionItem value="reference">
<AccordionTrigger>Reference</AccordionTrigger>
<AccordionContent className="text-sm">
<p>
Based on: "m-Eternal Domination and Variants on Some Classes of Finite and
Infinite Graphs" by Calamoneri et al. (CIAC 2025), which establishes bounds
and strategies for eternal domination on various grid types including square,
hexagonal, and triangular grids.
</p>
</AccordionContent>
</AccordionItem>
</Accordion>
<SocialShare
title="Eternal Domination on a Grid"
description="Explore the m-eternal domination problem - can you attack in a way that breaks the defense?"
/>
</div> </div>
</div> </div>
</Layout>
); );
}; };

View file

@ -1,27 +1,12 @@
import Layout from "@/components/Layout";
import FerrersRogersRamanujan from "@/components/FerrersRogersRamanujan"; import FerrersRogersRamanujan from "@/components/FerrersRogersRamanujan";
import SocialShare from "@/components/SocialShare";
const FerrersRogersRamanujanPage = () => { const FerrersRogersRamanujanPage = () => {
return ( return (
<Layout> <div className="min-h-screen bg-background">
<div className="container mx-auto py-8 px-4 space-y-8"> <div className="container mx-auto px-4 py-8">
<div className="space-y-2"> <FerrersRogersRamanujan showSocialShare={true} />
<h1 className="text-4xl font-bold">Ferrers Diagram & Rogers-Ramanujan Partition</h1>
<p className="text-muted-foreground text-lg">
Explore integer partitions through visual Ferrers diagrams and discover the elegant Rogers-Ramanujan transformation.
</p>
</div> </div>
<FerrersRogersRamanujan />
<SocialShare
title="Ferrers Diagram & Rogers-Ramanujan Partition"
description="Explore integer partitions through visual Ferrers diagrams and discover the elegant Rogers-Ramanujan transformation."
url="https://lovable.dev/themes/discrete-math/counting/ferrers-rogers-ramanujan"
/>
</div> </div>
</Layout>
); );
}; };

View file

@ -1,10 +1,11 @@
import React from 'react';
import GoldCoinGame from '@/components/GoldCoinGame'; import GoldCoinGame from '@/components/GoldCoinGame';
const GoldCoinGamePage = () => { const GoldCoinGamePage = () => {
return ( return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<GoldCoinGame /> <GoldCoinGame showSocialShare={true} />
</div>
</div> </div>
); );
}; };

View file

@ -1,29 +1,12 @@
import Layout from '@/components/Layout';
import LadybugClockPuzzle from '@/components/LadybugClockPuzzle'; import LadybugClockPuzzle from '@/components/LadybugClockPuzzle';
import { useLocation, Link } from 'react-router-dom';
import { ArrowLeft } from 'lucide-react';
const LadybugClockPuzzlePage = () => { const LadybugClockPuzzlePage = () => {
const location = useLocation();
const fromPuzzles = location.search.includes('from=puzzles');
return ( return (
<Layout> <div className="min-h-screen bg-background">
{fromPuzzles && ( <div className="container mx-auto px-4 py-8">
<div className="max-w-6xl mx-auto px-4 pt-4"> <LadybugClockPuzzle showSocialShare={true} />
<Link
to="/themes/puzzles"
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<ArrowLeft className="w-4 h-4 mr-1" />
Back to Puzzles
</Link>
</div> </div>
)}
<div className="py-8">
<LadybugClockPuzzle />
</div> </div>
</Layout>
); );
}; };

View file

@ -1,10 +1,11 @@
import React from 'react';
import NimGame from '@/components/NimGame'; import NimGame from '@/components/NimGame';
const NimGamePage = () => { const NimGamePage = () => {
return ( return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<NimGame /> <NimGame showSocialShare={true} />
</div>
</div> </div>
); );
}; };

View file

@ -1,11 +1,12 @@
import Layout from "@/components/Layout";
import ParityBitsGame from "@/components/ParityBitsGame"; import ParityBitsGame from "@/components/ParityBitsGame";
const ParityBitsGamePage = () => { const ParityBitsGamePage = () => {
return ( return (
<Layout> <div className="min-h-screen bg-background">
<ParityBitsGame /> <div className="container mx-auto px-4 py-8">
</Layout> <ParityBitsGame showSocialShare={true} />
</div>
</div>
); );
}; };

View file

@ -1,29 +1,13 @@
import PebblePlacementGame from "@/components/PebblePlacementGame"; import PebblePlacementGame from "@/components/PebblePlacementGame";
import { useSearchParams } from "react-router-dom";
const PebblePlacementGamePage = () => { const PebblePlacementGamePage = () => {
const [searchParams] = useSearchParams();
const from = searchParams.get('from');
if (from === 'puzzles') {
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8 space-y-6"> <div className="container mx-auto px-4 py-8">
<div className="flex items-center gap-4">
<button
onClick={() => window.location.href = '/themes/puzzles'}
className="text-primary hover:text-primary/80 font-medium"
>
Back to Puzzles
</button>
</div>
<PebblePlacementGame showSocialShare={true} /> <PebblePlacementGame showSocialShare={true} />
</div> </div>
</div> </div>
); );
}
return <PebblePlacementGame showSocialShare={true} />;
}; };
export default PebblePlacementGamePage; export default PebblePlacementGamePage;

View file

@ -1,13 +1,12 @@
import Layout from '@/components/Layout';
import PresentsPuzzle from "@/components/PresentsPuzzle"; import PresentsPuzzle from "@/components/PresentsPuzzle";
const PresentsPuzzlePage = () => { const PresentsPuzzlePage = () => {
return ( return (
<Layout> <div className="min-h-screen bg-background">
<div className="max-w-6xl mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<PresentsPuzzle showSocialShare={true} /> <PresentsPuzzle showSocialShare={true} />
</div> </div>
</Layout> </div>
); );
}; };

View file

@ -1,34 +1,10 @@
import { useState } from "react";
import RentDivisionPuzzle from "@/components/RentDivisionPuzzle"; import RentDivisionPuzzle from "@/components/RentDivisionPuzzle";
import SocialShare from "@/components/SocialShare";
const RentDivisionPuzzlePage = () => { const RentDivisionPuzzlePage = () => {
const [isCompleted, setIsCompleted] = useState(false);
return ( return (
<div <div className="min-h-screen bg-background">
className={`min-h-screen transition-colors duration-500 p-6 ${ <div className="container mx-auto px-4 py-8">
isCompleted ? "bg-green-50/50 dark:bg-green-950/20" : "bg-background" <RentDivisionPuzzle showSocialShare={true} />
}`}
>
<div className="max-w-6xl mx-auto space-y-6">
<div className="space-y-2">
<h1 className="text-3xl font-bold text-foreground">
Envy-Free Rent Division
</h1>
<p className="text-muted-foreground">
Explore fair division by assigning rooms and setting rents. Can you find an
allocation where no one envies another?
</p>
</div>
<RentDivisionPuzzle onComplete={setIsCompleted} />
<SocialShare
title="Envy-Free Rent Division - Interactive Fair Division"
description="Explore fair division by assigning rooms and setting rents to achieve envy-free allocations."
url="https://lovable.dev/themes/social-choice/rent-division"
/>
</div> </div>
</div> </div>
); );

View file

@ -1,31 +1,10 @@
import { useState } from "react";
import RulesOfInferencePlayground from "@/components/RulesOfInferencePlayground"; import RulesOfInferencePlayground from "@/components/RulesOfInferencePlayground";
import SocialShare from "@/components/SocialShare";
const RulesOfInferencePlaygroundPage = () => { const RulesOfInferencePlaygroundPage = () => {
const [isCompleted, setIsCompleted] = useState(false);
return ( return (
<div <div className="min-h-screen bg-background">
className={`min-h-screen transition-colors duration-500 p-6 ${ <div className="container mx-auto px-4 py-8">
isCompleted ? 'bg-green-50/50 dark:bg-green-950/20' : 'bg-background' <RulesOfInferencePlayground showSocialShare={true} />
}`}
>
<div className="max-w-4xl mx-auto space-y-6">
<div className="space-y-2">
<h1 className="text-3xl font-bold text-foreground">Rules of Inference Playground</h1>
<p className="text-muted-foreground">
Practice applying resolution and equivalence rules to derive conclusions step by step.
</p>
</div>
<RulesOfInferencePlayground onComplete={setIsCompleted} />
<SocialShare
title="Rules of Inference Playground - Interactive Logic Learning"
description="Practice applying resolution and equivalence rules to derive conclusions step by step in this interactive logic playground."
url="https://lovable.dev/discrete-math/foundations/rules-of-inference"
/>
</div> </div>
</div> </div>
); );

View file

@ -1,29 +1,13 @@
import StackingBlocks from "@/components/StackingBlocks"; import StackingBlocks from "@/components/StackingBlocks";
import { useSearchParams } from "react-router-dom";
const StackingBlocksPage = () => { const StackingBlocksPage = () => {
const [searchParams] = useSearchParams();
const from = searchParams.get('from');
if (from === 'puzzles') {
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8 space-y-6"> <div className="container mx-auto px-4 py-8">
<div className="flex items-center gap-4">
<button
onClick={() => window.location.href = '/themes/puzzles'}
className="text-primary hover:text-primary/80 font-medium"
>
Back to Puzzles
</button>
</div>
<StackingBlocks showSocialShare={true} /> <StackingBlocks showSocialShare={true} />
</div> </div>
</div> </div>
); );
}
return <StackingBlocks showSocialShare={true} />;
}; };
export default StackingBlocksPage; export default StackingBlocksPage;

View file

@ -1,7 +1,13 @@
import SubtractionGame from '@/components/SubtractionGame'; import SubtractionGame from '@/components/SubtractionGame';
const SubtractionGamePage = () => { const SubtractionGamePage = () => {
return <SubtractionGame />; return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8">
<SubtractionGame showSocialShare={true} />
</div>
</div>
);
}; };
export default SubtractionGamePage; export default SubtractionGamePage;

View file

@ -2,9 +2,9 @@ import TernaryNumberGame from '@/components/TernaryNumberGame';
const TernaryNumberGamePage = () => { const TernaryNumberGamePage = () => {
return ( return (
<div className="min-h-screen bg-background p-6"> <div className="min-h-screen bg-background">
<div className="max-w-4xl mx-auto"> <div className="container mx-auto px-4 py-8">
<TernaryNumberGame /> <TernaryNumberGame showSocialShare={true} />
</div> </div>
</div> </div>
); );

View file

@ -2,9 +2,9 @@ import ZeckendorfGame from '@/components/ZeckendorfGame';
const ZeckendorfGamePage = () => { const ZeckendorfGamePage = () => {
return ( return (
<div className="min-h-screen bg-background p-6"> <div className="min-h-screen bg-background">
<div className="max-w-4xl mx-auto"> <div className="container mx-auto px-4 py-8">
<ZeckendorfGame /> <ZeckendorfGame showSocialShare={true} />
</div> </div>
</div> </div>
); );

View file

@ -2,9 +2,9 @@ import ZeckendorfSearchTrick from '@/components/ZeckendorfSearchTrick';
const ZeckendorfSearchTrickPage = () => { const ZeckendorfSearchTrickPage = () => {
return ( return (
<div className="min-h-screen bg-background p-6"> <div className="min-h-screen bg-background">
<div className="max-w-6xl mx-auto"> <div className="container mx-auto px-4 py-8">
<ZeckendorfSearchTrick /> <ZeckendorfSearchTrick showSocialShare={true} />
</div> </div>
</div> </div>
); );