A retro-themed Nintendo (NES) controller simulator with real-time input logging and cheat code detection. Built with Next.js, TypeScript, and a terminal/CRT aesthetic.
- 🎮 Interactive NES Controller - Click/tap buttons or use keyboard controls
- 📊 Real-time Input Logging - Track all button presses with timestamps and input sources
- 🎯 Cheat Code Detection - Automatically detects classic cheat codes (Konami Code, ABBA, etc.)
- 🎨 Retro Terminal Theme - CRT scanlines, emerald terminal aesthetic, pixel fonts
- 📱 Mobile Responsive - Emulator-style layout on mobile, classic layout on desktop
- 🧪 TDD Setup - Full Jest + Testing Library infrastructure with style guards
- ♿ Accessible - ARIA labels, keyboard navigation, focus management
- Node.js 20+
- pnpm (recommended) or npm/yarn
# Clone the repository
git clone https://github.com/avicrayyy/nintroller.git
cd nintroller
# Install dependencies
pnpm install
# Run development server
pnpm devOpen http://localhost:3000 in your browser.
Keyboard Mapping:
- Arrow Keys - D-pad (Up, Down, Left, Right)
Z- B buttonX- A buttonShift- SelectEnter- Start
Mouse/Touch: Click or tap any button on the controller.
Try entering these sequences:
- Konami Code:
↑ ↑ ↓ ↓ ← → ← → B A Start - ABBA:
A B B A - Select + Start:
Select Start
When a cheat is detected, a modal will appear celebrating your discovery! 🎉
- Desktop: Input log appears as a fixed right sidebar
- Mobile: Tap the "LOG" FAB button in the top-right to open the log drawer
The log shows:
- Timestamp of each event
- Button pressed
- Press state (down/up)
- Input source (keyboard/pointer)
By default, cheat detection runs client-side for better performance, zero server costs, and offline support. However, if you need server-side detection (e.g., for analytics, rate limiting, or preventing client-side manipulation), you can enable it:
We chose client-side cheat detection as the default implementation for several important reasons:
-
Cost Optimization: Serverless functions on Vercel (and similar platforms) incur costs per invocation. For a portfolio piece that could receive unpredictable traffic, client-side detection eliminates API route costs entirely.
-
Performance: No network latency means instant cheat detection. Button presses are processed immediately without waiting for server round-trips.
-
Offline Support: The application works fully offline - users can detect cheats even without an internet connection.
-
Scalability: Client-side detection scales infinitely without server resources. No need to worry about rate limits, cold starts, or server capacity.
-
Abuse Prevention: By moving logic to the client, we eliminate the risk of API abuse (spamming endpoints, DDoS attempts, etc.) that could rack up unexpected costs.
Trade-offs: Client-side detection means the logic is visible in the browser and could theoretically be manipulated. For a portfolio/gaming project, this is acceptable. For production applications requiring security, use the server-side option (see below).
- Analytics: Track cheat usage patterns server-side
- Rate Limiting: Prevent abuse by limiting requests per session
- Security: Prevent client-side manipulation of cheat detection
- Multi-Instance Deployments: Centralized session management
- Add session ID state to
ControllerPlayground:
import { getOrCreateSessionId } from "@/app/utils";
// Inside ControllerPlayground component:
const [sessionId] = useState(() => getOrCreateSessionId());- Replace client-side detection with server-side API call:
import { detectCheatOnServer } from "@/app/libs/api/cheats";
// In onButtonChange handler:
if (e.pressed) {
try {
const result = await detectCheatOnServer(sessionId, e);
if (result.detected) {
setLastCheat({
id: result.detected.id,
name: result.detected.name,
});
window.dispatchEvent(
new CustomEvent("cheat-unlocked", {
detail: { cheat: result.detected },
})
);
setModalType("cheat");
}
} catch (error) {
console.error("Cheat detection failed:", error);
// Handle error (e.g., offline, rate limited)
}
}- See the commented example in
app/components/ControllerPlayground/index.tsxfor the full implementation.
- Endpoint:
POST /api/cheats - Abstraction Layer:
app/libs/api/cheats.ts(usedetectCheatOnServer()) - Session Storage: In-memory (not production-ready for multi-instance)
- For production, replace with durable storage (Redis, database, etc.)
- In serverless deployments, each instance has its own memory
- Sessions do not survive restarts
- Replace
SESSIONSMap with a shared store (Redis, database, etc.) - Implement proper session cleanup and TTL management
- Add rate limiting middleware
- Consider authentication/authorization if needed
# Development
pnpm dev # Start dev server
pnpm build # Build for production
pnpm start # Start production server
# Testing
pnpm test # Run tests once
pnpm test:watch # Run tests in watch mode
pnpm test:ci # Run tests in CI mode
# Code Quality
pnpm lint # Run ESLintThis project follows Next.js 16 App Router conventions with a component-based architecture. Here's a comprehensive overview:
nintroller/
├── app/ # Next.js App Router directory
│ ├── api/ # API routes
│ │ └── cheats/
│ │ └── route.ts # Cheat detection endpoint
│ │
│ ├── components/ # React components
│ │ ├── ControllerPlayground/ # Main playground orchestrator
│ │ │ └── index.tsx # Manages controller, modals, FABs
│ │ │
│ │ ├── NESController/ # NES Controller component
│ │ │ ├── index.tsx # Re-export shim
│ │ │ ├── NESController.tsx # Main controller UI
│ │ │ ├── BaseButton.tsx # Reusable button component
│ │ │ └── keyboard.ts # Keyboard event handlers
│ │ │
│ │ ├── InputLog/ # Input logging system
│ │ │ └── index.tsx # Component + Context Provider
│ │ │
│ │ ├── InputLogSidebar/ # Right sidebar (desktop) / modal (mobile)
│ │ │ └── index.tsx # Responsive sidebar component
│ │ │
│ │ ├── ObjectivesSidebar/ # Left sidebar for cheat objectives
│ │ │ └── index.tsx # Displays cheat list + progress
│ │ │
│ │ ├── ControllerConsoleCards/ # Console-style info cards
│ │ │ └── index.tsx # Warning & help cards
│ │ │
│ │ └── ui/ # Reusable UI components
│ │ ├── Button.tsx # Button component (variants: primary, secondary, emerald)
│ │ ├── IconButton/ # Icon button with FAB variant
│ │ │ └── index.tsx
│ │ └── Modal/ # Accessible modal component
│ │ ├── index.tsx # Modal wrapper
│ │ └── content/ # Modal content components
│ │ ├── WelcomeContent.tsx
│ │ ├── CheatContent.tsx
│ │ ├── ResetProgressContent.tsx
│ │ └── index.tsx
│ │
│ ├── hooks/ # Custom React hooks
│ │ ├── index.ts # Re-export shim for all hooks
│ │ ├── useSidebarState.ts # Sidebar state management
│ │ ├── useEscapeKey.ts # Escape key handler
│ │ ├── useFocusManagement.ts # Focus management
│ │ ├── useUnlockedCheats.ts # Unlocked cheats with localStorage
│ │ └── useSidebarToggleEvents.ts # Sidebar toggle event listeners
│ │
│ ├── libs/ # Business logic libraries
│ │ ├── cheats.ts # Cheat definitions & detection logic
│ │ └── api/
│ │ └── cheats.ts # Server-side cheat detection abstraction (optional)
│ │
│ ├── types/ # TypeScript type definitions
│ │ └── nes-controller.ts # NES controller types
│ │
│ ├── utils/ # Utility functions
│ │ └── index.ts # cx, prettyButtonName, getOrCreateSessionId
│ │
│ ├── globals.css # Global styles + Tailwind
│ ├── layout.tsx # Root layout (sidebars, providers)
│ └── page.tsx # Home page (renders ControllerPlayground)
│
├── __tests__/ # Test files
│ ├── components/ # Component tests
│ │ ├── Button.test.tsx
│ │ ├── IconButton.test.tsx
│ │ ├── InputLog.test.tsx
│ │ ├── InputLogSidebar.test.tsx
│ │ ├── ObjectivesSidebar.test.tsx
│ │ ├── ControllerPlayground.test.tsx
│ │ └── CheatModal.test.tsx
│ └── lib/ # Utility/library tests
│ ├── cheats.test.ts
│ ├── keyboard.test.ts
│ └── utils.test.ts
│
├── public/ # Static assets
│ └── demo-desktop.png # Desktop demo screenshot
│
└── [config files] # package.json, tsconfig.json, etc.
-
app/components/: All React components organized by feature- Each component lives in its own folder with an
index.tsxentry point ui/contains reusable components used across the app- Feature components (like
NESController/,InputLog/) are self-contained
- Each component lives in its own folder with an
-
app/libs/: Pure business logic (no React dependencies)- Cheat detection algorithms
- Data transformations
- Utility functions that don't depend on React
app/libs/api/: API abstraction layer- Encapsulates API route calls
- Type-safe request/response handling
- Optional server-side patterns (client-side by default)
- Co-located with business logic for better organization
-
app/types/: Shared TypeScript type definitions- Centralized types for better maintainability
- Used across components and utilities
-
app/hooks/: Custom React hooks- Reusable hooks for state management, event handling, and UI utilities
- Single responsibility principle
- Fully typed with TypeScript
- Examples:
useSidebarState,useUnlockedCheats,useEscapeKey
-
app/utils/: Shared utility functionscx()for className concatenation- Formatting helpers
- Client-side utilities (localStorage, etc.)
-
__tests__/: Test organization mirrors source structurecomponents/for React component testslib/for utility/library tests- Follows TDD principles with comprehensive coverage
- Folder-based organization: Each component has its own folder
- Re-export shims:
index.tsxfiles provide clean import paths - Context providers: Shared state via React Context (e.g.,
InputLogProvider) - Custom hooks: Reusable logic extracted into hooks (e.g.,
useSidebarState,useUnlockedCheats) - Event-driven communication: Custom events for cross-component updates
- Responsive design: Mobile-first with desktop enhancements
This project enforces:
- No
React.*namespace usage - Use direct imports (useState,useEffect, etc.) - Component folder structure - Components live at folder entrypoints
- TypeScript strict mode - Full type safety
- TDD approach - Tests written alongside features
See SESSION_NOTES.mdx for detailed development notes and architecture decisions.
- Framework: Next.js 16 (App Router)
- Language: TypeScript
- Styling: Tailwind CSS 4
- Testing: Jest + Testing Library
- Fonts: Press Start 2P (Google Fonts)
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Write tests for your changes
- Ensure all tests pass (
pnpm test) - Ensure linting passes (
pnpm lint) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is private and not licensed for public use.
- Inspired by classic NES controllers and retro gaming aesthetics
- Built with modern web technologies for a nostalgic experience
Made with ❤️ and lots of button presses
