Compare commits
2 Commits
245ab6414a
...
7758b31d6b
| Author | SHA1 | Date | |
|---|---|---|---|
| 7758b31d6b | |||
| 90d50bf1c2 |
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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) && (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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} />
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user