commit d687c6b77f9dcbee27619e67d85a0ee3ddc8c847 Author: dnviti Date: Sun Dec 14 21:00:46 2025 +0100 Initial Commit diff --git a/.agent/rules/development-folders.md b/.agent/rules/development-folders.md new file mode 100644 index 0000000..e5d4a8c --- /dev/null +++ b/.agent/rules/development-folders.md @@ -0,0 +1,25 @@ +--- +trigger: always_on +--- + +## Documentation and Work Logging +You are required to use the `./docs/development/devlog` directory to track all work plans and their current status individually; within this directory, create logs using the strict filename format `yyyy-mm-dd-hh24miss_very_brief_description`. Additionally, use the `./docs/development` directory to maintain a summary `CENTRAL.md` file containing links to specific files within `./docs/development/devlog` alongside a brief synthesis of the development status. + +## Source Code Organization +Use `./src` as the root for the monolithic **Node.js (TypeScript)** solution. +The project follows a **Modular Monolith** pattern. All backend logic is structured by modules, while frontend code (React) resides within a client directory or is served as static assets from the node application. + +## Backend and Frontend Integration (The Monolith) +The core server project (e.g., `./src/server` or `./src/app`) contains the entry point (`index.ts` or `main.ts`). Functionality is divided into **Modules**: + +* **Controllers:** `./src/modules/[ModuleName]/controllers/` (Handle HTTP requests). +* **Routes:** `./src/modules/[ModuleName]/routes/` (Define express/fastify routes). +* **DTOs:** `./src/modules/[ModuleName]/dtos/` (Data Transfer Objects for validation). +* **Static Assets:** `./src/public/` (for module-specific assets if necessary). + +## Domain Layer +Shared business logic and database entities reside in shared directories or within the modules themselves, designed to be importable: + +* **Entities:** `./src/modules/[ModuleName]/entities/` (ORM definitions, e.g., TypeORM/Prisma models). +* **Services:** `./src/modules/[ModuleName]/services/` (Business logic implementation). +* **Interfaces:** `./src/shared/interfaces/` or within the module (Type definitions). diff --git a/.agent/rules/development-guide.md b/.agent/rules/development-guide.md new file mode 100644 index 0000000..782b7b9 --- /dev/null +++ b/.agent/rules/development-guide.md @@ -0,0 +1,21 @@ +--- +trigger: always_on +--- + +## Planning and Workflow +You must always start by producing an implementation plan in the dedicated folders and proposing to the user that they view it by default. If the user chooses to proceed, you are to continue with the implementation of the plan without stopping. It is essential that you update the plan continuously as the development progresses to reflect the current state of the project. + +## Code Philosophy and Integration +You are required to work with the existing code, specifically utilizing the logic found in the provided gemini-generated.js file—including its regex parsing, Scryfall data fetching, caching strategies, and pack generation algorithms—as the core of the "Draft Preparation Phase". You must refactor this monolithic component into modular services, such as a `CardParserService` and `PackGeneratorService`, to effectively separate the user interface from the business logic, making the system suitable for a multiplayer backend state. The software must be extremely optimized and easy to use; while the preparation phase happens on the client side, the live draft state must be synchronized via the backend. The average user, acting as the Draft Host, must be guided through every operation, from uploading a list and fetching data to configuring packs and opening a lobby for multiplayer. + +## UI/UX Design and Tech Stack +The graphics must be professional, immersive, and reassuring, utilizing a dark mode or gaming theme that replicates the visual style of the provided file (using Tailwind and Lucide icons) while elevating it to a production standard. The interface must include shortcuts for quick usage, and data saving must be immediate without requiring the user to click "Save" buttons. You must implement the specific views found in the provided code, including List View, Grid View, and the 3D perspective Stack View, ensuring they work seamlessly in a multiplayer context. The development will utilize **React** for the frontend, reusing component logic like `StackView` and `PackCard`, and **Node.js with TypeScript** for the backend. You must use **Socket.IO** to handle the real-time multiplayer draft state for synchronizing pack passing between clients, and the frontend generator must produce a JSON object of packs to be sent to the backend to initialize the session. + +## Documentation and Architecture +For every implementation, update, or modification request, you must create a detailed work plan to be followed strictly. If a request implies complex development, such as synchronizing drag-and-drop actions across multiple clients, you must warn the user beforehand. Before starting, always write a file dedicated to the request and the created plan within the `./docs/development` folder, and keep this file updated throughout the execution of the work plan. The platform must be organized into applications managed via a sidebar, such as the Cube Manager (using the adapted logic), Lobby Manager, and Live Draft interface. Each application must be toggleable and purchasable via an internal administration section. + +## Design System, Navigation, and Viewport +Interfaces must always be created using Material Design principles adapted for a dark gaming UI, with icons that clearly identify the purpose of the application or function. The interface must be fully responsive, with specific handling for mobile devices, such as converting the "Stack View" to a "Swipe View" where necessary. The menu must be a lateral sidebar with two levels: the first for active applications and the second for functions within those applications. A top search bar must be implemented as an accelerator to search for cards, players, or lobbies hierarchically. The main viewport must be configurable with tabs; every application opens as a new tab, and upon restarting, the tabs open last time must remain active. Specifically, starting a draft should open a new tab for the "Live Lobby," and the system must handle persistence to reconnect users to their active session upon reloading. + +## Localization, Database, and Error Handling +The management system is currently supporting English only. diff --git a/.agent/rules/functional-goal.md b/.agent/rules/functional-goal.md new file mode 100644 index 0000000..d79ee33 --- /dev/null +++ b/.agent/rules/functional-goal.md @@ -0,0 +1,15 @@ +--- +trigger: always_on +--- + +## Core Objective and User Experience +The primary objective is to engineer a high-performance, browser-based Magic: The Gathering Multiplayer Draft Simulator that bridges the gap between utilitarian management tools and immersive, game-like user experiences. The platform must be specifically designed to cater to the "Average User" acting as a Host, who requires a friction-free setup process without technical hurdles, and the "Player," who demands a snappy, visually engaging drafting environment accessible on any device. The goal is to provide a seamless transition from deck preparation to active gameplay, ensuring that the software feels responsive and robust, much like a native application rather than a static webpage. + +## The Cube Manager and Preparation Phase +A central functional domain is the "Cube Manager," which handles the preparation phase. The system must provide automated ingestion capabilities that accept raw text lists—such as Arena exports or bulk text—and intelligently parse them using specific regex logic to identify card quantities and names. Following parsing, the system is required to seamlessly fetch and enrich card metadata, including images, rarity, and set information, from the Scryfall API while implementing robust caching to ensure speed. This data drives a sophisticated Pack Generation engine that creates "Booster Packs" based on customizable rules, such as "Peasant" distribution or "Chaos" modes, while automatically filtering out unplayable tokens or basic lands. Users must be empowered to verify their generated pools through distinct visualizations, including List, Grid, and a 3D perspective "Stack" view, before committing to a game. + +## The Live Draft and Multiplayer Synchronization +The core gameplay revolves around the "Live Draft" phase, which necessitates a real-time multiplayer environment where eight or more players pass packs simultaneously. You must implement a synchronization engine (via **Socket.IO**) to ensure that state changes, such as picking a card or passing a pack, are instantly propagated across all connected clients with millisecond precision. The interface must support intuitive interactions, allowing players to draft via drag-and-drop or click-to-pick mechanisms, with built-in logic to auto-sort picked cards by color or mana cost. Crucially, the system must handle network latency and interruptions gracefully; if a player disconnects or refreshes the page, they must be immediately reconnected to the exact state of their active pick without any data loss. + +## Ecosystem, Responsiveness, and Session Management +The user experience must mimic a comprehensive operating system through a tabbed multitasking interface, allowing users to keep the Live Draft open in one tab while managing the Cube in another within the same viewport. The platform operates on a modular "App Store" model, where functional areas like Tournaments, Brackets, or Collection management can be toggled or activated via an admin panel. The interface must be rigorously responsive, employing a mobile-first adaptation strategy that transforms wide desktop column views into touch-friendly carousels or swipe interfaces on smaller screens. Furthermore, all user actions, from deck building to cube editing, must trigger zero-friction auto-saving to backend databases via AJAX/Fetch calls, eliminating the need for manual save buttons and ensuring total data integrity throughout the session. diff --git a/.agent/rules/running-guide.md b/.agent/rules/running-guide.md new file mode 100644 index 0000000..1fed3e3 --- /dev/null +++ b/.agent/rules/running-guide.md @@ -0,0 +1,5 @@ +--- +trigger: always_on +--- + +Everytime there is a new feature or something was changed, run the application and test every new feature in (also in browser) to avoid future bugs. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f1a90a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,141 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.* +!.env.example + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist +.output + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Sveltekit cache directory +.svelte-kit/ + +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# Firebase cache directory +.firebase/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v3 +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Vite files +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +.vite/ \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ac5e821 --- /dev/null +++ b/Makefile @@ -0,0 +1,50 @@ +# MTG Draft Maker - Node.js Monolith + +# Variables +PROJECT_DIR := src + +.PHONY: help install dev dev-server dev-client build start clean + +# Default target +help: + @echo "======================================================================" + @echo " MTG Draft Maker - Node.js Monolith" + @echo "======================================================================" + @echo " make install : Install dependencies" + @echo " make dev : Run both Server and Client (Development)" + @echo " make dev-server : Run only Server (Development)" + @echo " make dev-client : Run only Client (Development)" + @echo " make build : Build Backend and Frontend for Production" + @echo " make start : Run built application (Production)" + @echo " make clean : Clean node_modules and build artifacts" + @echo "======================================================================" + +install: + @echo ">>> Installing dependencies..." + cd $(PROJECT_DIR) && npm install + +dev: + @echo ">>> Starting Development Environment..." + cd $(PROJECT_DIR) && npm run dev + +dev-server: + @echo ">>> Starting Backend (Dev)..." + cd $(PROJECT_DIR) && npm run server + +dev-client: + @echo ">>> Starting Frontend (Dev)..." + cd $(PROJECT_DIR) && npm run client + +build: + @echo ">>> Building for Production..." + cd $(PROJECT_DIR) && npm run build + +start: + @echo ">>> Starting Production Server..." + cd $(PROJECT_DIR) && npm run start + +clean: + @echo ">>> Cleaning artifacts..." + rm -rf $(PROJECT_DIR)/node_modules + rm -rf $(PROJECT_DIR)/dist + rm -rf $(PROJECT_DIR)/client/dist diff --git a/docs/development/CENTRAL.md b/docs/development/CENTRAL.md new file mode 100644 index 0000000..16ba368 --- /dev/null +++ b/docs/development/CENTRAL.md @@ -0,0 +1,17 @@ +# Development Central Log + +## Status Overview +The project has successfully migrated from a .NET backend to a Node.js Modular Monolith. The core "Draft Preparation" and "Tournament Bracket" functionalities have been implemented in the frontend using React, adhering to the reference design. + +## Recent Updates +- **[2025-12-14] Core Implementation**: Refactored `gemini-generated.js` into modular services and components. Implemented Cube Manager and Tournament Manager. [Link](./devlog/2025-12-14-194558_core_implementation.md) +- **[2025-12-14] Parser Robustness**: Improving `CardParserService` to handle formats without Scryfall IDs (e.g., Arena exports). [Link](./devlog/2025-12-14-210000_fix_parser_robustness.md) + +## Active Modules +1. **Cube Manager**: Fully functional (Parsing, Fetching, Pack Generation). +2. **Tournament Manager**: Basic Bracket generation implemented. + +## Roadmap +1. **Backend Integration**: Connect frontend generation to backend via Socket.IO. +2. **Live Draft**: Implement the multiplayer drafting interface. +3. **User Session**: Handle host/player sessions. diff --git a/docs/development/devlog/2025-12-14-190500_initial_project_setup.md b/docs/development/devlog/2025-12-14-190500_initial_project_setup.md new file mode 100644 index 0000000..656514a --- /dev/null +++ b/docs/development/devlog/2025-12-14-190500_initial_project_setup.md @@ -0,0 +1,33 @@ +# PROJ001: Initial Project Setup and Logic Refactoring (Node.js Migration) + +## Status: COMPLETED + +### Achievements +- **Architecture**: Pivoted from .NET to a **Node.js Monolith** structure to natively support real-time state synchronization via Socket.IO. +- **Frontend Infrastructure**: Configured **React** 19 + **Vite** + **Tailwind CSS** (v3) in `src/client`. +- **Backend Infrastructure**: Initialized **Express** server with **Socket.IO** in `src/server` for handling API requests and multiplayer draft state. +- **Refactoring**: Successfully ported legacy `gemini-generated.js` logic into specialized TypeScript services: + - `CardParserService.ts`: Regex-based list parsing. + - `ScryfallService.ts`: Data fetching with caching. + - `PackGeneratorService.ts`: Pack creation logic. +- **UI Implementation**: Developed `CubeManager`, `PackCard`, and `StackView` components. +- **Cleanup**: Removed all .NET artifacts and dependencies. +- **Tooling**: Updated `Makefile` for unified Node.js development commands. + +### How to Run +- **Install**: `make install` (or `cd src && npm install`) +- **Run Development**: `make dev` (Runs Server and Client concurrently) +- **Build**: `make build` + +### Manual Verification Steps +1. **Run**: `make dev` +2. **Access**: Open `http://localhost:5173` (Client). +3. **Test**: + - Click "Load Demo List" in the Cube Manager. + - Verify cards are fetched from Scryfall. + - Click "Generate Pools". + - Verify packs are generated and visible in Stack/Grid views. + +### Next Steps +- Implement `DraftSession` state management in `src/server`. +- Define Socket.IO events for lobby creation and player connection. diff --git a/docs/development/devlog/2025-12-14-193500_migration_to_nodejs.md b/docs/development/devlog/2025-12-14-193500_migration_to_nodejs.md new file mode 100644 index 0000000..708b717 --- /dev/null +++ b/docs/development/devlog/2025-12-14-193500_migration_to_nodejs.md @@ -0,0 +1,29 @@ +# Migration to Node.js Backend + +## Objective +Convert the project from a .NET backend to a Node.js (TypeScript) backend and remove the .NET infrastructure. + +## Plan + +### Phase 1: Structure Initialization +- [ ] Initialize `src` as a Node.js project (`package.json`, `tsconfig.json`). +- [ ] Create directory structure: + - [ ] `src/server`: Backend logic. + - [ ] `src/client`: Move existing React frontend here. + - [ ] `src/shared`: Shared interfaces/types. + +### Phase 2: React Frontend Migration +- [ ] Move `src/MtgDraft.Web/Client` contents to `src/client/src`. +- [ ] Move configuration files (`vite.config.ts`, `tailwind.config.js`, etc.) to `src/client` root or adjust as needed. +- [ ] Ensure frontend builds and runs via Vite (dev server). + +### Phase 3: Node.js Backend Implementation +- [ ] Set up Express/Fastify server in `src/server/index.ts`. +- [ ] Configure Socket.IO foundations. +- [ ] Configure build scripts to build client and server. + +### Phase 4: Verification +- [ ] Verify application runs with `npm run dev`. + +### Phase 5: Cleanup +- [ ] Delete `MtgDraft.*` folders. diff --git a/docs/development/devlog/2025-12-14-194558_core_implementation.md b/docs/development/devlog/2025-12-14-194558_core_implementation.md new file mode 100644 index 0000000..29b0b73 --- /dev/null +++ b/docs/development/devlog/2025-12-14-194558_core_implementation.md @@ -0,0 +1,30 @@ +# Implementation of Core Functionalities + +## Status +Completed + +## Description +Implemented the core functionalities based on the reference `gemini-generated.js` file, refactoring the monolithic logic into a modular architecture. + +## Changes +1. **Services**: + - Created `CardParserService` for parsing bulk text lists. + - Created `ScryfallService` for fetching card data with caching and batching. + - Created `PackGeneratorService` for generating booster packs with various rules (Peasant, Standard, Chaos). + +2. **Modules**: + - **CubeManager**: Implemented the Draft Preparation Phase UI (Input, Filters, Generation). + - **TournamentManager**: Implemented the Tournament Bracket generation logic and UI. + +3. **Components**: + - `PackCard`: card component with List, Grid, and Stack views. + - `StackView`: 3D card stack visualization. + - `TournamentPackView`: "Blind Mode" / Box view for generated packs. + +4. **Architecture**: + - Created `App.tsx` as the main shell with Tab navigation (Draft vs Bracket). + - Integrated all components into the main entry point. + +## Next Steps +- Integrate Socket.IO for real-time draft synchronization (Multiplayer). +- Implement the "Live Draft" interface. diff --git a/docs/development/devlog/2025-12-14-203000_fix_root_render_generation.md b/docs/development/devlog/2025-12-14-203000_fix_root_render_generation.md new file mode 100644 index 0000000..bf7b5a5 --- /dev/null +++ b/docs/development/devlog/2025-12-14-203000_fix_root_render_generation.md @@ -0,0 +1,19 @@ +# Bug Fix: React Render Error and Pack Generation Stability + +## Issue +User reported "root.render(" error visible on page and "Generate Packs" button ineffective. + +## Diagnosis +1. **main.tsx**: Found nested `root.render( root.render(...) )` call. This caused runtime errors and visible artifact text. +2. **CubeManager.tsx**: Service classes (`ScryfallService`, `PackGeneratorService`) were instantiated inside the functional component body without `useMemo`. This caused recreation on every render, leading to cache loss (`ScryfallService` internal cache) and potential state inconsistencies. +3. **Pack Generation**: Double-clicking or rapid state updates caused "phantom" generation runs with empty pools, resetting the packs list to 0 immediately after success. + +## Resolution +1. **Fixed main.tsx**: Removed the nested `root.render` call. +2. **Refactored CubeManager.tsx**: + * Memoized all services using `useMemo`. + * Added `loading` state to `generatePacks` to prevent double-submissions. + * Wrapped generation logic in `setTimeout` to allow UI updates and `try/catch` for robustness. + +## Status +Verified via browser subagent (logs confirmed 241 packs generated). UI now prevents race conditions. diff --git a/docs/development/devlog/2025-12-14-210000_fix_parser_robustness.md b/docs/development/devlog/2025-12-14-210000_fix_parser_robustness.md new file mode 100644 index 0000000..e3caf48 --- /dev/null +++ b/docs/development/devlog/2025-12-14-210000_fix_parser_robustness.md @@ -0,0 +1,24 @@ +# Bug Fix: Card Parser Robustness + +## User Request +"The problem is that if the scryfall id is missing, no card is retrieved so no card is generated, instead the system should be able to retrieve cards and generate packs even without scryfall id" + +## Diagnosis +The `CardParserService` currently performs basic name extraction. It fails to strip set codes and collector numbers common in export formats (e.g., MTG Arena exports like `1 Shock (M20) 160`). +This causes `ScryfallService` to search for "Shock (M20) 160" as an exact name, which fails. The system relies on successful Scryfall matches to populate the card pool; without matches, the pool is empty, and generation produces 0 packs. + +## Implementation Plan +1. **Refactor `CardParserService.ts`**: + * Enhance regex to explicitly handle and strip: + * Parentheses containing text (e.g., `(M20)`). + * Collector numbers at the end of lines. + * Set codes in square brackets if present. + * Maintain support for `Quantity Name` format. + * Ensure exact name cleanup to maximize Scryfall "exact match" hits. + +2. **Verification**: + * Create a test input imitating Arena export. + * Verify via browser subagent that cards are fetched and packs are generated. + +## Update Central +Update `CENTRAL.md` with this task. diff --git a/src/client/index.html b/src/client/index.html new file mode 100644 index 0000000..7982a1f --- /dev/null +++ b/src/client/index.html @@ -0,0 +1,12 @@ + + + + + + MTG Draft Maker + + +
+ + + diff --git a/src/client/src/App.tsx b/src/client/src/App.tsx new file mode 100644 index 0000000..b58b0dd --- /dev/null +++ b/src/client/src/App.tsx @@ -0,0 +1,44 @@ +import React, { useState } from 'react'; +import { Layers, Box, Trophy } from 'lucide-react'; +import { CubeManager } from './modules/cube/CubeManager'; +import { TournamentManager } from './modules/tournament/TournamentManager'; + +export const App: React.FC = () => { + const [activeTab, setActiveTab] = useState<'draft' | 'bracket'>('draft'); + + return ( +
+
+
+
+
+
+

MTG Peasant Drafter

+

Pack Generator & Tournament Manager

+
+
+ +
+ + +
+
+
+ +
+ {activeTab === 'draft' && } + {activeTab === 'bracket' && } +
+
+ ); +}; diff --git a/src/client/src/components/PackCard.tsx b/src/client/src/components/PackCard.tsx new file mode 100644 index 0000000..1d970dc --- /dev/null +++ b/src/client/src/components/PackCard.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import { DraftCard, Pack } from '../services/PackGeneratorService'; +import { Copy } from 'lucide-react'; +import { StackView } from './StackView'; + +interface PackCardProps { + pack: Pack; + viewMode: 'list' | 'grid' | 'stack'; +} + +const ListItem: React.FC<{ card: DraftCard }> = ({ card }) => { + const getRarityColorClass = (rarity: string) => { + switch (rarity) { + case 'common': return 'bg-black text-white border-slate-600'; + case 'uncommon': return 'bg-slate-300 text-slate-900 border-white'; + case 'rare': return 'bg-yellow-500 text-yellow-950 border-yellow-200'; + case 'mythic': return 'bg-orange-600 text-white border-orange-300'; + default: return 'bg-slate-500'; + } + }; + + return ( +
  • +
    + + {card.name} + + +
    + {card.image && ( +
    +
    + {card.name} +
    +
    + )} +
  • + ); +}; + +export const PackCard: React.FC = ({ pack, viewMode }) => { + const mythics = pack.cards.filter(c => c.rarity === 'mythic'); + const rares = pack.cards.filter(c => c.rarity === 'rare'); + const uncommons = pack.cards.filter(c => c.rarity === 'uncommon'); + const commons = pack.cards.filter(c => c.rarity === 'common'); + + const copyPackToClipboard = () => { + const text = pack.cards.map(c => c.name).join('\n'); + navigator.clipboard.writeText(text); + // Toast notification could go here + alert(`Pack list ${pack.id} copied!`); + }; + + return ( +
    + {/* Header */} +
    +
    +

    Pack #{pack.id}

    + {pack.setName} +
    + +
    + + {/* Content */} +
    + {viewMode === 'list' && ( +
    + {(mythics.length > 0 || rares.length > 0) && ( +
    +
    Rare / Mythic ({mythics.length + rares.length})
    +
      + {mythics.map(card => )} + {rares.map(card => )} +
    +
    + )} +
    +
    Uncommons ({uncommons.length})
    +
      + {uncommons.map(card => )} +
    +
    +
    +
    Commons ({commons.length})
    +
      + {commons.map(card => )} +
    +
    +
    + )} + + {viewMode === 'grid' && ( +
    + {pack.cards.map((card) => ( +
    + {card.image ? ( + {card.name} + ) : ( +
    + {card.name} +
    + )} +
    +
    + ))} +
    + )} + + {viewMode === 'stack' && } +
    +
    + ); +}; diff --git a/src/client/src/components/StackView.tsx b/src/client/src/components/StackView.tsx new file mode 100644 index 0000000..95f22fe --- /dev/null +++ b/src/client/src/components/StackView.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { DraftCard } from '../services/PackGeneratorService'; + +interface StackViewProps { + cards: DraftCard[]; +} + +export const StackView: React.FC = ({ cards }) => { + const getRarityColorClass = (rarity: string) => { + switch (rarity) { + case 'common': return 'bg-black text-white border-slate-600'; + case 'uncommon': return 'bg-slate-300 text-slate-900 border-white'; + case 'rare': return 'bg-yellow-500 text-yellow-950 border-yellow-200'; + case 'mythic': return 'bg-orange-600 text-white border-orange-300'; + default: return 'bg-slate-500'; + } + }; + + return ( +
    +
    + {cards.map((card, index) => { + const colorClass = getRarityColorClass(card.rarity); + // Random slight rotation for "organic" look + const rotation = (index % 2 === 0 ? 1 : -1) * (Math.random() * 2); + + return ( +
    + {card.image ? ( + {card.name} + ) : ( +
    + {card.name} +
    + )} +
    +
    + ); + })} +
    +
    + Hover to expand stack +
    +
    + ); +}; diff --git a/src/client/src/components/TournamentPackView.tsx b/src/client/src/components/TournamentPackView.tsx new file mode 100644 index 0000000..1ed6a53 --- /dev/null +++ b/src/client/src/components/TournamentPackView.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { Target, Package, Layers } from 'lucide-react'; +import { Pack } from '../services/PackGeneratorService'; + +interface TournamentPackViewProps { + packs: Pack[]; +} + +export const TournamentPackView: React.FC = ({ packs }) => { + const packsBySet = packs.reduce((acc, pack) => { + const key = pack.setName || 'Unknown Set'; + if (!acc[key]) acc[key] = []; + acc[key].push(pack); + return acc; + }, {} as { [key: string]: Pack[] }); + + const BOX_SIZE = 30; + + return ( +
    + {Object.entries(packsBySet).map(([setName, setPacks]) => { + const boxes = []; + for (let i = 0; i < setPacks.length; i += BOX_SIZE) boxes.push(setPacks.slice(i, i + BOX_SIZE)); + + return ( +
    +
    +
    +

    + {setName} +

    +
    +
    + +
    + {boxes.map((boxPacks, boxIndex) => ( +
    +
    + BOX {boxIndex + 1} + ({boxPacks.length} packs) +
    + +
    + {boxPacks.map((pack) => ( +
    +
    + + MTG +
    +
    +
    #{pack.id}
    +
    {pack.setName}
    +
    + +
    +
    +

    Contains {pack.cards.length} cards:

    + {pack.cards.some(c => c.rarity === 'mythic' || c.rarity === 'rare') && ( +

    ★ Rare / Mythic

    + )} +

    Click for full list

    +
    +
    +
    + ))} +
    +
    + ))} +
    +
    + ); + })} +
    + ); +}; diff --git a/src/client/src/main.tsx b/src/client/src/main.tsx new file mode 100644 index 0000000..eb5d7e2 --- /dev/null +++ b/src/client/src/main.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { App } from './App'; +import './styles/main.css'; + +const rootElement = document.getElementById('root'); + +if (rootElement) { + const root = createRoot(rootElement); + root.render( + + {/* is now part of App */} + + + ); +} diff --git a/src/client/src/modules/cube/CubeManager.tsx b/src/client/src/modules/cube/CubeManager.tsx new file mode 100644 index 0000000..9e57e8f --- /dev/null +++ b/src/client/src/modules/cube/CubeManager.tsx @@ -0,0 +1,336 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { Layers, RotateCcw, Box, Check, Loader2, Upload, Eye, EyeOff, LayoutGrid, List, Sliders, Settings } from 'lucide-react'; +import { CardParserService } from '../../services/CardParserService'; +import { ScryfallService, ScryfallCard } from '../../services/ScryfallService'; +import { PackGeneratorService, ProcessedPools, SetsMap, Pack, PackGenerationSettings } from '../../services/PackGeneratorService'; +import { PackCard } from '../../components/PackCard'; +import { TournamentPackView } from '../../components/TournamentPackView'; + +export const CubeManager: React.FC = () => { + // --- Services --- + // --- Services --- + // Memoize services to persist cache across renders + const parserService = React.useMemo(() => new CardParserService(), []); + const scryfallService = React.useMemo(() => new ScryfallService(), []); + const generatorService = React.useMemo(() => new PackGeneratorService(), []); + + // --- State --- + const [inputText, setInputText] = useState(''); + const [loading, setLoading] = useState(false); + const [progress, setProgress] = useState(''); + + const [rawScryfallData, setRawScryfallData] = useState(null); + const [processedData, setProcessedData] = useState<{ pools: ProcessedPools, sets: SetsMap } | null>(null); + + const [filters, setFilters] = useState({ + ignoreBasicLands: true, + ignoreCommander: true, + ignoreTokens: true + }); + + const [packs, setPacks] = useState([]); + + // UI State + const [viewMode, setViewMode] = useState<'list' | 'grid' | 'stack'>('list'); + const [tournamentMode, setTournamentMode] = useState(false); + + // Generation Settings + const [genSettings, setGenSettings] = useState({ + mode: 'mixed', + rarityMode: 'peasant' + }); + + const fileInputRef = useRef(null); + + // --- Effects --- + useEffect(() => { + if (rawScryfallData) { + const result = generatorService.processCards(rawScryfallData, filters); + setProcessedData(result); + } + }, [filters, rawScryfallData]); + + // --- Handlers --- + const handleFileUpload = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (e) => setInputText(e.target?.result as string || ''); + reader.readAsText(file); + event.target.value = ''; + }; + + const loadDemoData = () => { + const demo = `20 Shock +20 Llanowar Elves +20 Giant Growth +20 Counterspell +20 Dark Ritual +20 Lightning Bolt +20 Opt +20 Consider +20 Ponder +20 Preordain +20 Brainstorm +20 Duress +20 Faithless Looting +20 Thrill of Possibility +20 Terror +10 Serra Angel +10 Vampire Nighthawk +10 Eternal Witness +10 Mulldrifter +10 Flametongue Kavu +5 Wrath of God +5 Birds of Paradise +2 Jace, the Mind Sculptor +1 Sheoldred, the Apocalypse +20 Island +1 Sol Ring +1 Command Tower`; + setInputText(demo); + }; + + const fetchAndParse = async () => { + setLoading(true); + setPacks([]); + setProgress('Parsing text...'); + setTournamentMode(false); + + try { + const identifiers = parserService.parse(inputText); + + // Expand quantity for fetching logic (though service handles it, let's just pass uniques to service) + // The service `fetchCollection` deduplicates automatically. + + // Map identifier interface + const fetchList = identifiers.map(id => id.type === 'id' ? { id: id.value } : { name: id.value }); + + // Fetch + await scryfallService.fetchCollection(fetchList, (current, total) => { + setProgress(`Fetching Scryfall data... (${current}/${total})`); + }); + + // Expand based on original quantities + const expandedCards: ScryfallCard[] = []; + identifiers.forEach(id => { + const card = scryfallService.getCachedCard(id.type === 'id' ? { id: id.value } : { name: id.value }); + if (card) { + for (let i = 0; i < id.quantity; i++) expandedCards.push(card); + } + }); + + setRawScryfallData(expandedCards); + // Processing happens via useEffect + + setLoading(false); + setProgress(''); + + } catch (err: any) { + console.error(err); + alert(err.message || "Error during process."); + setLoading(false); + } + }; + + const generatePacks = () => { + if (!processedData) return; + + if (processedData.pools.commons.length === 0 && processedData.pools.uncommons.length === 0 && processedData.pools.rares.length === 0) { + alert(`Not enough valid cards.`); + return; + } + + setLoading(true); + + // Use setTimeout to allow UI to show loading spinner before sync calculation blocks + setTimeout(() => { + try { + const newPacks = generatorService.generatePacks(processedData.pools, processedData.sets, genSettings); + + if (newPacks.length === 0) { + alert(`Not enough cards to generate valid packs (minimum required for selected mode).`); + } else { + setPacks(newPacks); + setTournamentMode(false); + } + } catch (e) { + console.error("Generation failed", e); + alert("Error generating packs: " + e); + } finally { + setLoading(false); + } + }, 50); + }; + + const toggleFilter = (key: keyof typeof filters) => { + setFilters(prev => ({ ...prev, [key]: !prev[key] })); + }; + + return ( +
    + + {/* --- LEFT COLUMN: CONTROLS --- */} +
    +
    +
    + +
    + + + +
    +
    + + {/* Filters */} +
    +

    + Import Options +

    +
    + + + +
    +
    + +