Compare commits

..

2 Commits

7 changed files with 130 additions and 87 deletions

View File

@@ -74,3 +74,6 @@
- [Play Online Logic](./devlog/2025-12-17-031500_play_online_logic.md): Completed. Implemented strict pack limits (min 12 for 4 players) and visual feedback for the online lobby button.
- [Lobby Rules Tooltip](./devlog/2025-12-17-032000_lobby_rules_tooltip.md): Completed. Added dynamic rules explanation and supported player indicators to the lobby creation screen.
- [Fix Expansion Pack Generation](./devlog/2025-12-17-140000_fix_expansion_generation.md): Completed. Enforced infinite card pool for expansion drafts to ensure correct pack counts and prevent depletion.
- [Responsive Pack Grid Layout](./devlog/2025-12-17-142500_responsive_pack_grid.md): Completed. Implemented responsive multi-column grid for generated packs when card size is reduced (<25% slider).
- [Stack View Consistency Fix](./devlog/2025-12-17-143000_stack_view_consistency.md): Completed. Removed transparent overrides for Stack View, ensuring it renders with the standard unified container graphic.
- [Dynamic Pack Grid Layout](./devlog/2025-12-17-144000_dynamic_pack_grid.md): Completed. Implemented responsive CSS grid with `minmax(550px, 1fr)` for Stack/Grid views to auto-fit packs based on screen width without explicit column limits.

View File

@@ -0,0 +1,20 @@
# Responsive Pack Grid Layout
## Objective
Update the generated packs UI to maximize pack density on screen when the user reduces the card size.
## Requirements
- When the card size slider is under 25% (value <= 150), switch the pack container layout from a vertical stack (`grid-cols-1`) to a responsive multi-column grid (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3` etc.).
- Ensure this applies to all view modes (List, Grid, Stack).
- Maintain consistency in UI.
## Implementation
- Modified `src/client/src/modules/cube/CubeManager.tsx`.
- Added conditional logic to the main packs container `div`.
- Condition: `cardWidth <= 150`.
- Classes: `grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4` for compact mode.
## Verification
- Verified using browser simulation.
- Verified that setting slider to 100 triggers the grid layout.
- Verified that setting slider to 300 reverts to vertical stack.

View File

@@ -0,0 +1,17 @@
# Stack View Consistency Fix
## Objective
Ensure the Stack View pack container has the same visual styling (background, border, shadow, header) as the List and Grid views.
## User Request
"the stacked view region graphic is not consistent with the other views, the container region is missing"
## Implementation
- Modified `src/client/src/components/PackCard.tsx`.
- Removed the conditional ternary operators that stripped the background and border when `viewMode === 'stack'`.
- Ensured consistent `p-4` padding for the content wrapper.
- The `StackView` component is now rendered inside the standard slate card container.
## Verification
- Code review confirms the removal of `bg-transparent border-none` overrides.
- This ensures the `bg-slate-800` class applied to the parent `div` is visible in all modes.

View File

@@ -0,0 +1,19 @@
# Dynamic Pack Grid Layout
## Objective
Implement a truly dynamic, screen-dependent pack grid layout for Stack and Grid views to satisfy the requirement: "implement the grid to have dynamic number of packs in a single row based on the screen width".
## User Request
"only for the stacked view we need to avoid the horizontal scrollbar, meaning that 4 packs in a row is too much, for the stacked view the packs on a single row should be 2."
"now implement the grid to have dynamic number of packs in a single row based on the screen width"
## Implementation
- Modified `src/client/src/modules/cube/CubeManager.tsx`.
- Abandoned fixed Tailwind grid classes (`grid-cols-X`) for dynamic inline styles.
- Utilized CSS Grid `repeat(auto-fill, minmax(..., 1fr))` syntax.
- **Rules per view**:
- **List View**: `minmax(320px, 1fr)`. Allows multiple compact columns (up to 4+ on ultrawide).
- **Stack/Grid View**: `minmax(550px, 1fr)`. Guarantees wider columns. On a standard 1080p width (~1500px available), this results in **2 columns**. On 4K screens, it will auto-expand to 3 or 4 columns, preventing wasted space while respecting the density request.
## Verification
- Screenshots `stack_dynamic_final` and `grid_dynamic_final` confirm that on the test resolution, the layout successfully restricts to a readable grid without overflowing horizontal scrollbars.

View File

@@ -57,9 +57,9 @@ export const PackCard: React.FC<PackCardProps> = ({ pack, viewMode, cardWidth =
};
return (
<div className={`bg-slate-800 rounded-xl border border-slate-700 shadow-lg flex flex-col ${viewMode === 'stack' ? 'bg-transparent border-none shadow-none' : ''}`}>
<div className="bg-slate-800 rounded-xl border border-slate-700 shadow-lg flex flex-col">
{/* Header */}
<div className={`p-3 bg-slate-900 border-b border-slate-700 flex justify-between items-center rounded-t-xl ${viewMode === 'stack' ? 'bg-slate-800 border border-slate-700 mb-4 rounded-xl' : ''}`}>
<div className="p-3 bg-slate-900 border-b border-slate-700 flex justify-between items-center rounded-t-xl">
<div className="flex flex-col">
<h3 className="font-bold text-purple-400 text-sm md:text-base">Pack #{pack.id}</h3>
<span className="text-xs text-slate-500 font-mono">{pack.setName}</span>
@@ -74,7 +74,7 @@ export const PackCard: React.FC<PackCardProps> = ({ pack, viewMode, cardWidth =
</div>
{/* Content */}
<div className={`${viewMode !== 'stack' ? 'p-4' : ''}`}>
<div className="p-4 overflow-x-auto">
{viewMode === 'list' && (
<div className="text-sm space-y-4">
{(mythics.length > 0 || rares.length > 0) && (

View File

@@ -54,7 +54,7 @@ export const StackView: React.FC<StackViewProps> = ({ cards, cardWidth = 150 })
}, [cards]);
return (
<div className="flex flex-row gap-2 overflow-x-auto pb-8 snap-x">
<div className="flex flex-row gap-4 overflow-x-auto pb-8 snap-x">
{CATEGORY_ORDER.map(category => {
const catCards = categorizedCards[category];
if (catCards.length === 0) return null;

View File

@@ -175,27 +175,30 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
const fetchAndParse = async () => {
const handleGenerate = async () => {
// Validate inputs
if (sourceMode === 'set' && selectedSets.length === 0) return;
if (sourceMode === 'upload' && !inputText) return;
setLoading(true);
setPacks([]);
setProgress(sourceMode === 'set' ? 'Fetching set data...' : 'Parsing text...');
setPacks([]); // Clear old packs to avoid confusion
try {
let expandedCards: ScryfallCard[] = [];
// --- Step 1: Fetch/Parse ---
let currentCards: ScryfallCard[] = [];
setProgress(sourceMode === 'set' ? 'Fetching set data...' : 'Parsing text...');
if (sourceMode === 'set') {
if (selectedSets.length === 0) throw new Error("Please select at least one set.");
// We fetch set by set to show progress
// Fetch set by set
for (const [index, setCode] of selectedSets.entries()) {
setProgress(`Fetching set ${setCode.toUpperCase()} (${index + 1}/${selectedSets.length})...`);
const response = await fetch(`/api/sets/${setCode}/cards`);
if (!response.ok) throw new Error(`Failed to fetch set ${setCode}`);
const cards: ScryfallCard[] = await response.json();
expandedCards.push(...cards);
currentCards.push(...cards);
}
} else {
// Parse Text
setProgress('Parsing and fetching from server...');
@@ -210,36 +213,18 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
throw new Error(err.error || "Failed to parse cards");
}
expandedCards = await response.json();
currentCards = await response.json();
}
setRawScryfallData(expandedCards);
setLoading(false);
setProgress('');
// Update local state for UI preview/stats
setRawScryfallData(currentCards);
} catch (err: any) {
console.error(err);
alert(err.message || "Error during process.");
setLoading(false);
}
};
// --- Step 2: Generate ---
setProgress('Generating packs on server...');
const generatePacks = async () => {
// if (!processedData) return; // Logic moved to server, but we still use processedData for UI check
if (!rawScryfallData || rawScryfallData.length === 0) {
if (sourceMode === 'set' && selectedSets.length > 0) {
// Allowed to proceed if sets selected (server fetches)
} else {
return;
}
}
setLoading(true);
setProgress('Generating packs on server...');
try {
const payload = {
cards: sourceMode === 'upload' ? rawScryfallData : [],
cards: sourceMode === 'upload' ? currentCards : [], // For set mode, we let server refetch or handle it
sourceMode,
selectedSets,
settings: {
@@ -251,7 +236,6 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
filters
};
// Use fetch from server logic
if (sourceMode === 'set') {
payload.cards = [];
}
@@ -274,9 +258,9 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
} else {
setPacks(newPacks);
}
} catch (e: any) {
console.error("Generation failed", e);
alert("Error generating packs: " + e.message);
} catch (err: any) {
console.error("Process failed", err);
alert(err.message || "Error during process.");
} finally {
setLoading(false);
setProgress('');
@@ -434,13 +418,7 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
disabled={loading}
/>
<button
onClick={fetchAndParse}
disabled={loading || !inputText}
className={`w-full py-2 mb-4 rounded-lg font-bold flex justify-center items-center gap-2 transition-all ${loading ? 'bg-slate-700 cursor-not-allowed' : 'bg-purple-600 hover:bg-purple-500 text-white'}`}
>
{loading ? <><Loader2 className="w-4 h-4 animate-spin" /> {progress}</> : <><Check className="w-4 h-4" /> 1. Parse Bulk</>}
</button>
{/* Parse Button Removed per request */}
</>
) : (
<>
@@ -559,34 +537,12 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
</div>
</div>
<div className="mb-4">
<label className="block text-sm font-semibold text-slate-300 mb-2">Quantity</label>
<div className="flex items-center gap-2 bg-slate-900 p-2 rounded-lg border border-slate-700">
<input
type="number"
min={1}
max={20}
value={numBoxes}
onChange={(e) => setNumBoxes(parseInt(e.target.value))}
className="w-16 bg-slate-800 border-none rounded p-1 text-center text-white font-mono"
disabled={loading}
/>
<span className="text-slate-400 text-sm">Booster Boxes ({numBoxes * 36} Packs)</span>
</div>
</div>
<button
onClick={fetchAndParse}
disabled={loading || selectedSets.length === 0}
className={`w-full py-2 mb-4 rounded-lg font-bold flex justify-center items-center gap-2 transition-all ${loading ? 'bg-slate-700 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-500 text-white'}`}
>
{loading ? <><Loader2 className="w-4 h-4 animate-spin" /> {progress}</> : <><Check className="w-4 h-4" /> 1. Fetch {selectedSets.length > 1 ? 'Sets' : 'Set'}</>}
</button>
{/* Fetch Set and Quantity Blocks Removed/Moved */}
</>
)}
{/* Generation Settings */}
{processedData && Object.keys(processedData.sets).length > 0 && (
{(sourceMode === 'set' ? selectedSets.length > 0 : !!inputText) && (
<div className="bg-slate-900/50 p-3 rounded-lg border border-slate-700 mb-4 animate-in fade-in slide-in-from-top-4 duration-500">
<h3 className="text-sm font-bold text-white mb-2 flex items-center gap-2">
<Settings className="w-4 h-4 text-emerald-400" /> Configuration
@@ -630,25 +586,46 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
</div>
</div>
{/* Sets Info */}
<div className="max-h-40 overflow-y-auto text-xs space-y-1 pr-2 custom-scrollbar border-t border-slate-800 pt-2">
{Object.values(processedData.sets).sort((a, b) => b.commons.length - a.commons.length).map(set => (
<div key={set.code} className="flex justify-between items-center text-slate-400 border-b border-slate-800 pb-1">
<span className="truncate w-32" title={set.name}>{set.name}</span>
<span className="font-mono text-[10px]">{set.commons.length}C / {set.uncommons.length}U / {set.rares.length}R</span>
{/* Quantity - Moved Here */}
{sourceMode === 'set' && (
<div className="mb-4">
<label className="text-xs font-bold text-slate-400 uppercase mb-1 block">Quantity</label>
<div className="flex items-center gap-2 bg-slate-800 p-2 rounded border border-slate-700">
<input
type="number"
min={1}
max={20}
value={numBoxes}
onChange={(e) => setNumBoxes(parseInt(e.target.value))}
className="w-16 bg-slate-700 border-none rounded p-1 text-center text-white font-mono"
disabled={loading}
/>
<span className="text-slate-300 text-xs">Boxes ({numBoxes * 36} Packs)</span>
</div>
))}
</div>
</div>
)}
{/* Sets Info */}
{processedData && Object.keys(processedData.sets).length > 0 && (
<div className="max-h-40 overflow-y-auto text-xs space-y-1 pr-2 custom-scrollbar border-t border-slate-800 pt-2">
{Object.values(processedData.sets).sort((a, b) => b.commons.length - a.commons.length).map(set => (
<div key={set.code} className="flex justify-between items-center text-slate-400 border-b border-slate-800 pb-1">
<span className="truncate w-32" title={set.name}>{set.name}</span>
<span className="font-mono text-[10px]">{set.commons.length}C / {set.uncommons.length}U / {set.rares.length}R</span>
</div>
))}
</div>
)}
</div>
)}
<button
onClick={generatePacks}
disabled={!processedData || Object.keys(processedData.sets).length === 0 || loading}
className={`w-full py-3 px-4 rounded-lg font-bold flex justify-center items-center gap-2 transition-all ${!processedData || Object.keys(processedData.sets).length === 0 || loading ? 'bg-slate-700 cursor-not-allowed text-slate-500' : 'bg-emerald-600 hover:bg-emerald-500 text-white shadow-lg shadow-emerald-900/20'}`}
onClick={handleGenerate}
disabled={((sourceMode === 'set' && selectedSets.length === 0) || (sourceMode === 'upload' && !inputText)) || loading}
className={`w-full py-3 px-4 rounded-lg font-bold flex justify-center items-center gap-2 transition-all ${((sourceMode === 'set' && selectedSets.length === 0) || (sourceMode === 'upload' && !inputText)) || loading ? 'bg-slate-700 cursor-not-allowed text-slate-500' : 'bg-emerald-600 hover:bg-emerald-500 text-white shadow-lg shadow-emerald-900/20'}`}
>
{loading ? <Loader2 className="w-5 h-5 animate-spin" /> : <RotateCcw className="w-5 h-5" />}
{loading ? 'Generating...' : '2. Generate Packs'}
{loading ? progress : 'Generate Packs'}
</button>
{/* Reset Button */}
@@ -734,7 +711,14 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
<p>No packs generated.</p>
</div>
) : (
<div className="grid grid-cols-1 gap-6 pb-20">
<div
className="grid gap-6 pb-20"
style={{
gridTemplateColumns: cardWidth <= 150
? `repeat(auto-fill, minmax(${viewMode === 'list' ? '320px' : '550px'}, 1fr))`
: '1fr'
}}
>
{packs.map((pack) => (
<PackCard key={pack.id} pack={pack} viewMode={viewMode} cardWidth={cardWidth} />
))}