feat: Unify card fetching/parsing and pack generation into a single handleGenerate function and button.
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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) && (
|
||||||
|
|||||||
@@ -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} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user