feat: Persist app tab, generated packs, cube settings, and player data to local storage, and add a session reset option.
All checks were successful
Build and Deploy / build (push) Successful in 1m11s
All checks were successful
Build and Deploy / build (push) Successful in 1m11s
This commit is contained in:
@@ -7,8 +7,32 @@ import { DeckTester } from './modules/tester/DeckTester';
|
||||
import { Pack } from './services/PackGeneratorService';
|
||||
|
||||
export const App: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<'draft' | 'bracket' | 'lobby' | 'tester'>('draft');
|
||||
const [generatedPacks, setGeneratedPacks] = useState<Pack[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<'draft' | 'bracket' | 'lobby' | 'tester'>(() => {
|
||||
const saved = localStorage.getItem('activeTab');
|
||||
return (saved as 'draft' | 'bracket' | 'lobby' | 'tester') || 'draft';
|
||||
});
|
||||
|
||||
const [generatedPacks, setGeneratedPacks] = useState<Pack[]>(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem('generatedPacks');
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
} catch (e) {
|
||||
console.error("Failed to load packs from storage", e);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
localStorage.setItem('activeTab', activeTab);
|
||||
}, [activeTab]);
|
||||
|
||||
React.useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem('generatedPacks', JSON.stringify(generatedPacks));
|
||||
} catch (e) {
|
||||
console.error("Failed to save packs to storage", e);
|
||||
}
|
||||
}, [generatedPacks]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-900 text-slate-100 font-sans pb-20">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Layers, RotateCcw, Box, Check, Loader2, Upload, LayoutGrid, List, Sliders, Settings, Users, Download, Copy, FileDown } from 'lucide-react';
|
||||
import { Layers, RotateCcw, Box, Check, Loader2, Upload, LayoutGrid, List, Sliders, Settings, Users, Download, Copy, FileDown, Trash2 } from 'lucide-react';
|
||||
import { CardParserService } from '../../services/CardParserService';
|
||||
import { ScryfallService, ScryfallCard, ScryfallSet } from '../../services/ScryfallService';
|
||||
import { PackGeneratorService, ProcessedPools, SetsMap, Pack, PackGenerationSettings } from '../../services/PackGeneratorService';
|
||||
@@ -20,33 +20,91 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
|
||||
const generatorService = React.useMemo(() => new PackGeneratorService(), []);
|
||||
|
||||
// --- State ---
|
||||
const [inputText, setInputText] = useState('');
|
||||
const [inputText, setInputText] = useState(() => localStorage.getItem('cube_inputText') || '');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [progress, setProgress] = useState('');
|
||||
const [copySuccess, setCopySuccess] = useState(false);
|
||||
|
||||
const [rawScryfallData, setRawScryfallData] = useState<ScryfallCard[] | null>(null);
|
||||
const [rawScryfallData, setRawScryfallData] = useState<ScryfallCard[] | null>(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem('cube_rawScryfallData');
|
||||
return saved ? JSON.parse(saved) : null;
|
||||
} catch (e) {
|
||||
console.warn("Failed to load rawScryfallData from local storage", e);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (rawScryfallData) {
|
||||
localStorage.setItem('cube_rawScryfallData', JSON.stringify(rawScryfallData));
|
||||
} else {
|
||||
localStorage.removeItem('cube_rawScryfallData');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to save rawScryfallData to local storage (likely quota exceeded)", e);
|
||||
}
|
||||
}, [rawScryfallData]);
|
||||
const [processedData, setProcessedData] = useState<{ pools: ProcessedPools, sets: SetsMap } | null>(null);
|
||||
|
||||
const [filters, setFilters] = useState({
|
||||
const [filters, setFilters] = useState<{
|
||||
ignoreBasicLands: boolean;
|
||||
ignoreCommander: boolean;
|
||||
ignoreTokens: boolean;
|
||||
}>(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem('cube_filters');
|
||||
return saved ? JSON.parse(saved) : {
|
||||
ignoreBasicLands: true,
|
||||
ignoreCommander: true,
|
||||
ignoreTokens: true
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
ignoreBasicLands: true,
|
||||
ignoreCommander: true,
|
||||
ignoreTokens: true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// UI State
|
||||
const [viewMode, setViewMode] = useState<'list' | 'grid' | 'stack'>('list');
|
||||
|
||||
// Generation Settings
|
||||
const [genSettings, setGenSettings] = useState<PackGenerationSettings>({
|
||||
const [genSettings, setGenSettings] = useState<PackGenerationSettings>(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem('cube_genSettings');
|
||||
return saved ? JSON.parse(saved) : {
|
||||
mode: 'mixed',
|
||||
rarityMode: 'peasant'
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
mode: 'mixed',
|
||||
rarityMode: 'peasant'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const [sourceMode, setSourceMode] = useState<'upload' | 'set'>('upload');
|
||||
const [sourceMode, setSourceMode] = useState<'upload' | 'set'>(() =>
|
||||
(localStorage.getItem('cube_sourceMode') as 'upload' | 'set') || 'upload'
|
||||
);
|
||||
const [availableSets, setAvailableSets] = useState<ScryfallSet[]>([]);
|
||||
const [selectedSet, setSelectedSet] = useState<string>('');
|
||||
const [numBoxes, setNumBoxes] = useState<number>(3);
|
||||
const [selectedSet, setSelectedSet] = useState(() => localStorage.getItem('cube_selectedSet') || '');
|
||||
const [numBoxes, setNumBoxes] = useState<number>(() => {
|
||||
const saved = localStorage.getItem('cube_numBoxes');
|
||||
return saved ? parseInt(saved) : 3;
|
||||
});
|
||||
|
||||
// --- Persistence Effects ---
|
||||
useEffect(() => localStorage.setItem('cube_inputText', inputText), [inputText]);
|
||||
useEffect(() => localStorage.setItem('cube_filters', JSON.stringify(filters)), [filters]);
|
||||
useEffect(() => localStorage.setItem('cube_genSettings', JSON.stringify(genSettings)), [genSettings]);
|
||||
useEffect(() => localStorage.setItem('cube_sourceMode', sourceMode), [sourceMode]);
|
||||
useEffect(() => localStorage.setItem('cube_selectedSet', selectedSet), [selectedSet]);
|
||||
useEffect(() => localStorage.setItem('cube_numBoxes', numBoxes.toString()), [numBoxes]);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -216,6 +274,20 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
|
||||
setFilters(prev => ({ ...prev, [key]: !prev[key] }));
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
if (window.confirm("Are you sure you want to clear this session? All parsed cards and generated packs will be lost.")) {
|
||||
setPacks([]);
|
||||
setInputText('');
|
||||
setRawScryfallData(null);
|
||||
setProcessedData(null);
|
||||
setSelectedSet('');
|
||||
localStorage.removeItem('cube_inputText');
|
||||
localStorage.removeItem('cube_rawScryfallData');
|
||||
localStorage.removeItem('cube_selectedSet');
|
||||
// We keep filters and settings as they are user preferences
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-12 gap-8 p-4 md:p-6">
|
||||
|
||||
@@ -408,6 +480,15 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
|
||||
{loading ? <Loader2 className="w-5 h-5 animate-spin" /> : <RotateCcw className="w-5 h-5" />}
|
||||
{loading ? 'Generating...' : '2. Generate Packs'}
|
||||
</button>
|
||||
|
||||
{/* Reset Button */}
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="w-full mt-4 py-2 text-xs font-semibold text-slate-500 hover:text-red-400 hover:bg-red-900/10 rounded-lg transition-colors flex items-center justify-center gap-2"
|
||||
title="Clear all data and start over"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" /> Clear Session
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,11 +11,22 @@ interface LobbyManagerProps {
|
||||
|
||||
export const LobbyManager: React.FC<LobbyManagerProps> = ({ generatedPacks }) => {
|
||||
const [activeRoom, setActiveRoom] = useState<any>(null);
|
||||
const [playerName, setPlayerName] = useState('');
|
||||
const [playerName, setPlayerName] = useState(() => localStorage.getItem('player_name') || '');
|
||||
const [joinRoomId, setJoinRoomId] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [playerId] = useState(() => Math.random().toString(36).substring(2) + Date.now().toString(36)); // Simple persistent ID
|
||||
const [playerId] = useState(() => {
|
||||
const saved = localStorage.getItem('player_id');
|
||||
if (saved) return saved;
|
||||
const newId = Math.random().toString(36).substring(2) + Date.now().toString(36);
|
||||
localStorage.setItem('player_id', newId);
|
||||
return newId;
|
||||
});
|
||||
|
||||
// Persist player name
|
||||
React.useEffect(() => {
|
||||
localStorage.setItem('player_name', playerName);
|
||||
}, [playerName]);
|
||||
|
||||
const connect = () => {
|
||||
if (!socketService.socket.connected) {
|
||||
|
||||
@@ -34,7 +34,7 @@ export class CardParserService {
|
||||
const qty = parseInt(parts[0]);
|
||||
// If valid CSV structure
|
||||
if (!isNaN(qty)) {
|
||||
const name = parts[1]; // We can keep name for reference, but we use ID if present
|
||||
// const name = parts[1]; // We can keep name for reference, but we use ID if present
|
||||
const finishRaw = parts[2]?.toLowerCase();
|
||||
const finish = (finishRaw === 'foil' || finishRaw === 'etched') ? 'foil' : (finishRaw === 'normal' ? 'normal' : undefined);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user