feat: Unify card fetching/parsing and pack generation into a single handleGenerate function and button.

This commit is contained in:
2025-12-17 14:37:49 +01:00
parent 245ab6414a
commit 90d50bf1c2
5 changed files with 107 additions and 86 deletions

View File

@@ -74,3 +74,5 @@
- [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. - [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. - [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. - [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.

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

@@ -57,9 +57,9 @@ export const PackCard: React.FC<PackCardProps> = ({ pack, viewMode, cardWidth =
}; };
return ( 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 */} {/* 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"> <div className="flex flex-col">
<h3 className="font-bold text-purple-400 text-sm md:text-base">Pack #{pack.id}</h3> <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> <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> </div>
{/* Content */} {/* Content */}
<div className={`${viewMode !== 'stack' ? 'p-4' : ''}`}> <div className="p-4 overflow-x-auto">
{viewMode === 'list' && ( {viewMode === 'list' && (
<div className="text-sm space-y-4"> <div className="text-sm space-y-4">
{(mythics.length > 0 || rares.length > 0) && ( {(mythics.length > 0 || rares.length > 0) && (

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); setLoading(true);
setPacks([]); setPacks([]); // Clear old packs to avoid confusion
setProgress(sourceMode === 'set' ? 'Fetching set data...' : 'Parsing text...');
try { try {
let expandedCards: ScryfallCard[] = []; // --- Step 1: Fetch/Parse ---
let currentCards: ScryfallCard[] = [];
setProgress(sourceMode === 'set' ? 'Fetching set data...' : 'Parsing text...');
if (sourceMode === 'set') { if (sourceMode === 'set') {
if (selectedSets.length === 0) throw new Error("Please select at least one set."); // Fetch set by set
// We fetch set by set to show progress
for (const [index, setCode] of selectedSets.entries()) { for (const [index, setCode] of selectedSets.entries()) {
setProgress(`Fetching set ${setCode.toUpperCase()} (${index + 1}/${selectedSets.length})...`); setProgress(`Fetching set ${setCode.toUpperCase()} (${index + 1}/${selectedSets.length})...`);
const response = await fetch(`/api/sets/${setCode}/cards`); const response = await fetch(`/api/sets/${setCode}/cards`);
if (!response.ok) throw new Error(`Failed to fetch set ${setCode}`); if (!response.ok) throw new Error(`Failed to fetch set ${setCode}`);
const cards: ScryfallCard[] = await response.json(); const cards: ScryfallCard[] = await response.json();
expandedCards.push(...cards); currentCards.push(...cards);
} }
} else { } else {
// Parse Text // Parse Text
setProgress('Parsing and fetching from server...'); 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"); throw new Error(err.error || "Failed to parse cards");
} }
expandedCards = await response.json(); currentCards = await response.json();
} }
setRawScryfallData(expandedCards); // Update local state for UI preview/stats
setLoading(false); setRawScryfallData(currentCards);
setProgress('');
} catch (err: any) { // --- Step 2: Generate ---
console.error(err); setProgress('Generating packs on server...');
alert(err.message || "Error during process.");
setLoading(false);
}
};
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 = { const payload = {
cards: sourceMode === 'upload' ? rawScryfallData : [], cards: sourceMode === 'upload' ? currentCards : [], // For set mode, we let server refetch or handle it
sourceMode, sourceMode,
selectedSets, selectedSets,
settings: { settings: {
@@ -251,7 +236,6 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
filters filters
}; };
// Use fetch from server logic
if (sourceMode === 'set') { if (sourceMode === 'set') {
payload.cards = []; payload.cards = [];
} }
@@ -274,9 +258,9 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
} else { } else {
setPacks(newPacks); setPacks(newPacks);
} }
} catch (e: any) { } catch (err: any) {
console.error("Generation failed", e); console.error("Process failed", err);
alert("Error generating packs: " + e.message); alert(err.message || "Error during process.");
} finally { } finally {
setLoading(false); setLoading(false);
setProgress(''); setProgress('');
@@ -434,13 +418,7 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
disabled={loading} disabled={loading}
/> />
<button {/* Parse Button Removed per request */}
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>
</> </>
) : ( ) : (
<> <>
@@ -559,34 +537,12 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
</div> </div>
</div> </div>
<div className="mb-4"> {/* Fetch Set and Quantity Blocks Removed/Moved */}
<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>
</> </>
)} )}
{/* Generation Settings */} {/* 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"> <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"> <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 <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>
</div> </div>
{/* Sets Info */} {/* Quantity - Moved Here */}
<div className="max-h-40 overflow-y-auto text-xs space-y-1 pr-2 custom-scrollbar border-t border-slate-800 pt-2"> {sourceMode === 'set' && (
{Object.values(processedData.sets).sort((a, b) => b.commons.length - a.commons.length).map(set => ( <div className="mb-4">
<div key={set.code} className="flex justify-between items-center text-slate-400 border-b border-slate-800 pb-1"> <label className="text-xs font-bold text-slate-400 uppercase mb-1 block">Quantity</label>
<span className="truncate w-32" title={set.name}>{set.name}</span> <div className="flex items-center gap-2 bg-slate-800 p-2 rounded border border-slate-700">
<span className="font-mono text-[10px]">{set.commons.length}C / {set.uncommons.length}U / {set.rares.length}R</span> <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>
</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> </div>
)} )}
<button <button
onClick={generatePacks} onClick={handleGenerate}
disabled={!processedData || Object.keys(processedData.sets).length === 0 || loading} 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 ${!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'}`} 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 ? <Loader2 className="w-5 h-5 animate-spin" /> : <RotateCcw className="w-5 h-5" />}
{loading ? 'Generating...' : '2. Generate Packs'} {loading ? progress : 'Generate Packs'}
</button> </button>
{/* Reset Button */} {/* Reset Button */}
@@ -734,7 +711,12 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
<p>No packs generated.</p> <p>No packs generated.</p>
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 gap-6 pb-20"> <div className={`grid gap-6 pb-20 ${cardWidth <= 150
? viewMode === 'list'
? 'grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4'
: 'grid-cols-1 2xl:grid-cols-2'
: 'grid-cols-1'
}`}>
{packs.map((pack) => ( {packs.map((pack) => (
<PackCard key={pack.id} pack={pack} viewMode={viewMode} cardWidth={cardWidth} /> <PackCard key={pack.id} pack={pack} viewMode={viewMode} cardWidth={cardWidth} />
))} ))}