feat: Implement and refine a Toast notification system, and replace the copy pack toast with an animated button.

This commit is contained in:
2025-12-17 02:22:53 +01:00
parent b0dc734859
commit 3194be382f
8 changed files with 227 additions and 57 deletions

View File

@@ -5,6 +5,7 @@ import { TournamentManager } from './modules/tournament/TournamentManager';
import { LobbyManager } from './modules/lobby/LobbyManager';
import { DeckTester } from './modules/tester/DeckTester';
import { Pack } from './services/PackGeneratorService';
import { ToastProvider } from './components/Toast';
export const App: React.FC = () => {
const [activeTab, setActiveTab] = useState<'draft' | 'bracket' | 'lobby' | 'tester'>(() => {
@@ -35,58 +36,60 @@ export const App: React.FC = () => {
}, [generatedPacks]);
return (
<div className="h-screen flex flex-col bg-slate-900 text-slate-100 font-sans overflow-hidden">
<header className="bg-slate-800 border-b border-slate-700 p-4 shrink-0 z-50 shadow-lg">
<div className="max-w-7xl mx-auto flex flex-col md:flex-row justify-between items-center gap-4">
<div className="flex items-center gap-3">
<div className="bg-purple-600 p-2 rounded-lg"><Layers className="w-6 h-6 text-white" /></div>
<div>
<h1 className="text-2xl font-bold bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">MTG Peasant Drafter</h1>
<p className="text-slate-400 text-xs uppercase tracking-wider">Pack Generator & Tournament Manager</p>
<ToastProvider>
<div className="h-screen flex flex-col bg-slate-900 text-slate-100 font-sans overflow-hidden">
<header className="bg-slate-800 border-b border-slate-700 p-4 shrink-0 z-50 shadow-lg">
<div className="max-w-7xl mx-auto flex flex-col md:flex-row justify-between items-center gap-4">
<div className="flex items-center gap-3">
<div className="bg-purple-600 p-2 rounded-lg"><Layers className="w-6 h-6 text-white" /></div>
<div>
<h1 className="text-2xl font-bold bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">MTG Peasant Drafter</h1>
<p className="text-slate-400 text-xs uppercase tracking-wider">Pack Generator & Tournament Manager</p>
</div>
</div>
<div className="flex bg-slate-900 rounded-lg p-1 border border-slate-700">
<button
onClick={() => setActiveTab('draft')}
className={`px-3 md:px-4 py-2 rounded-md text-sm font-bold flex items-center gap-2 transition-all ${activeTab === 'draft' ? 'bg-purple-600 text-white' : 'text-slate-400 hover:text-white'}`}
>
<Box className="w-4 h-4" /> <span className="hidden md:inline">Draft Management</span>
</button>
<button
onClick={() => setActiveTab('lobby')}
className={`px-3 md:px-4 py-2 rounded-md text-sm font-bold flex items-center gap-2 transition-all ${activeTab === 'lobby' ? 'bg-emerald-600 text-white' : 'text-slate-400 hover:text-white'}`}
>
<Users className="w-4 h-4" /> <span className="hidden md:inline">Online Lobby</span>
</button>
<button
onClick={() => setActiveTab('tester')}
className={`px-3 md:px-4 py-2 rounded-md text-sm font-bold flex items-center gap-2 transition-all ${activeTab === 'tester' ? 'bg-teal-600 text-white' : 'text-slate-400 hover:text-white'}`}
>
<Play className="w-4 h-4" /> <span className="hidden md:inline">Deck Tester</span>
</button>
<button
onClick={() => setActiveTab('bracket')}
className={`px-3 md:px-4 py-2 rounded-md text-sm font-bold flex items-center gap-2 transition-all ${activeTab === 'bracket' ? 'bg-blue-600 text-white' : 'text-slate-400 hover:text-white'}`}
>
<Trophy className="w-4 h-4" /> <span className="hidden md:inline">Tournament</span>
</button>
</div>
</div>
</header>
<div className="flex bg-slate-900 rounded-lg p-1 border border-slate-700">
<button
onClick={() => setActiveTab('draft')}
className={`px-3 md:px-4 py-2 rounded-md text-sm font-bold flex items-center gap-2 transition-all ${activeTab === 'draft' ? 'bg-purple-600 text-white' : 'text-slate-400 hover:text-white'}`}
>
<Box className="w-4 h-4" /> <span className="hidden md:inline">Draft Management</span>
</button>
<button
onClick={() => setActiveTab('lobby')}
className={`px-3 md:px-4 py-2 rounded-md text-sm font-bold flex items-center gap-2 transition-all ${activeTab === 'lobby' ? 'bg-emerald-600 text-white' : 'text-slate-400 hover:text-white'}`}
>
<Users className="w-4 h-4" /> <span className="hidden md:inline">Online Lobby</span>
</button>
<button
onClick={() => setActiveTab('tester')}
className={`px-3 md:px-4 py-2 rounded-md text-sm font-bold flex items-center gap-2 transition-all ${activeTab === 'tester' ? 'bg-teal-600 text-white' : 'text-slate-400 hover:text-white'}`}
>
<Play className="w-4 h-4" /> <span className="hidden md:inline">Deck Tester</span>
</button>
<button
onClick={() => setActiveTab('bracket')}
className={`px-3 md:px-4 py-2 rounded-md text-sm font-bold flex items-center gap-2 transition-all ${activeTab === 'bracket' ? 'bg-blue-600 text-white' : 'text-slate-400 hover:text-white'}`}
>
<Trophy className="w-4 h-4" /> <span className="hidden md:inline">Tournament</span>
</button>
</div>
</div>
</header>
<main className="flex-1 overflow-hidden relative">
{activeTab === 'draft' && (
<CubeManager
packs={generatedPacks}
setPacks={setGeneratedPacks}
onGoToLobby={() => setActiveTab('lobby')}
/>
)}
{activeTab === 'lobby' && <LobbyManager generatedPacks={generatedPacks} />}
{activeTab === 'tester' && <DeckTester />}
{activeTab === 'bracket' && <TournamentManager />}
</main>
</div>
<main className="flex-1 overflow-hidden relative">
{activeTab === 'draft' && (
<CubeManager
packs={generatedPacks}
setPacks={setGeneratedPacks}
onGoToLobby={() => setActiveTab('lobby')}
/>
)}
{activeTab === 'lobby' && <LobbyManager generatedPacks={generatedPacks} />}
{activeTab === 'tester' && <DeckTester />}
{activeTab === 'bracket' && <TournamentManager />}
</main>
</div>
</ToastProvider>
);
};

View File

@@ -1,7 +1,8 @@
import React from 'react';
import { DraftCard, Pack } from '../services/PackGeneratorService';
import { Copy } from 'lucide-react';
import { Copy, Check } from 'lucide-react';
import { StackView } from './StackView';
import { CardHoverWrapper, FoilOverlay } from './CardPreview';
interface PackCardProps {
pack: Pack;
@@ -9,9 +10,6 @@ interface PackCardProps {
cardWidth?: number;
}
import { CardHoverWrapper, FoilOverlay } from './CardPreview';
const ListItem: React.FC<{ card: DraftCard }> = ({ card }) => {
const isFoil = (card: DraftCard) => card.finish === 'foil';
@@ -43,6 +41,7 @@ const ListItem: React.FC<{ card: DraftCard }> = ({ card }) => {
};
export const PackCard: React.FC<PackCardProps> = ({ pack, viewMode, cardWidth = 150 }) => {
const [copied, setCopied] = React.useState(false);
const mythics = pack.cards.filter(c => c.rarity === 'mythic');
const rares = pack.cards.filter(c => c.rarity === 'rare');
const uncommons = pack.cards.filter(c => c.rarity === 'uncommon');
@@ -53,7 +52,8 @@ export const PackCard: React.FC<PackCardProps> = ({ pack, viewMode, cardWidth =
const copyPackToClipboard = () => {
const text = pack.cards.map(c => c.name).join('\n');
navigator.clipboard.writeText(text);
alert(`Pack list ${pack.id} copied!`);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
@@ -64,8 +64,12 @@ export const PackCard: React.FC<PackCardProps> = ({ pack, viewMode, cardWidth =
<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>
</div>
<button onClick={copyPackToClipboard} className="text-slate-400 hover:text-white p-1 rounded hover:bg-slate-700 transition-colors flex items-center gap-2 text-xs">
<Copy className="w-4 h-4" />
<button
onClick={copyPackToClipboard}
className={`p-1.5 rounded transition-all duration-300 flex items-center gap-2 text-xs border ${copied ? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/50' : 'text-slate-400 border-transparent hover:text-white hover:bg-slate-700'}`}
title="Copy to clipboard"
>
{copied ? <Check className="w-4 h-4 scale-110 animate-in zoom-in spin-in-12 duration-300" /> : <Copy className="w-4 h-4" />}
</button>
</div>

View File

@@ -0,0 +1,85 @@
import React, { createContext, useContext, useState, useCallback } from 'react';
import { X, Check, AlertCircle, Info } from 'lucide-react';
type ToastType = 'success' | 'error' | 'info';
interface Toast {
id: string;
message: string;
type: ToastType;
}
interface ToastContextType {
showToast: (message: string, type?: ToastType) => void;
}
const ToastContext = createContext<ToastContextType | undefined>(undefined);
export const useToast = () => {
const context = useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
};
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [toasts, setToasts] = useState<Toast[]>([]);
const showToast = useCallback((message: string, type: ToastType = 'info') => {
const id = Math.random().toString(36).substring(2, 9);
setToasts((prev) => [...prev, { id, message, type }]);
// Auto remove after 3 seconds
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 3000);
}, []);
const removeToast = (id: string) => {
setToasts((prev) => prev.filter((t) => t.id !== id));
};
return (
<ToastContext.Provider value={{ showToast }}>
{children}
<div className="fixed top-6 left-1/2 -translate-x-1/2 z-[9999] flex flex-col gap-3 pointer-events-none w-full max-w-sm px-4">
{toasts.map((toast) => (
<div
key={toast.id}
className={`
pointer-events-auto
flex items-center gap-4 px-4 py-3 rounded-xl border shadow-2xl
animate-in slide-in-from-top-full fade-in zoom-in-95 duration-300
bg-slate-800 text-white
${toast.type === 'success' ? 'border-emerald-500/50 shadow-emerald-900/20' :
toast.type === 'error' ? 'border-red-500/50 shadow-red-900/20' :
'border-blue-500/50 shadow-blue-900/20'}
`}
>
<div className={`p-2 rounded-full shrink-0 ${toast.type === 'success' ? 'bg-emerald-500/10 text-emerald-400' :
toast.type === 'error' ? 'bg-red-500/10 text-red-400' :
'bg-blue-500/10 text-blue-400'
}`}>
{toast.type === 'success' && <Check className="w-5 h-5" />}
{toast.type === 'error' && <AlertCircle className="w-5 h-5" />}
{toast.type === 'info' && <Info className="w-5 h-5" />}
</div>
<div className="flex-1 text-sm font-medium">
{toast.message}
</div>
<button
onClick={() => removeToast(toast.id)}
className="p-1 hover:bg-slate-700 rounded transition-colors text-slate-400 hover:text-white"
>
<X className="w-4 h-4" />
</button>
</div>
))}
</div>
</ToastContext.Provider>
);
};