From 3194be382f3f43e2f67ac87358b9082a17ad8940 Mon Sep 17 00:00:00 2001 From: dnviti Date: Wed, 17 Dec 2025 02:22:53 +0100 Subject: [PATCH] feat: Implement and refine a Toast notification system, and replace the copy pack toast with an animated button. --- docs/development/CENTRAL.md | 4 + ...5-12-17-022000_replace_alert_with_toast.md | 16 +++ ...-12-17-023000_enhanced_toast_visibility.md | 21 ++++ .../2025-12-17-023500_unified_toast_design.md | 17 +++ .../2025-12-17-024000_animated_copy_button.md | 20 ++++ src/client/src/App.tsx | 103 +++++++++--------- src/client/src/components/PackCard.tsx | 18 +-- src/client/src/components/Toast.tsx | 85 +++++++++++++++ 8 files changed, 227 insertions(+), 57 deletions(-) create mode 100644 docs/development/devlog/2025-12-17-022000_replace_alert_with_toast.md create mode 100644 docs/development/devlog/2025-12-17-023000_enhanced_toast_visibility.md create mode 100644 docs/development/devlog/2025-12-17-023500_unified_toast_design.md create mode 100644 docs/development/devlog/2025-12-17-024000_animated_copy_button.md create mode 100644 src/client/src/components/Toast.tsx diff --git a/docs/development/CENTRAL.md b/docs/development/CENTRAL.md index 26293c2..05e0fd6 100644 --- a/docs/development/CENTRAL.md +++ b/docs/development/CENTRAL.md @@ -67,3 +67,7 @@ - [Change Default Filter Flags](./devlog/2025-12-17-025500_change_default_flags.md): Completed. Updated client and server defaults for "Ignore Basic Lands", "Ignore Commander Sets", and "Ignore Tokens" to be unchecked (false). - [Sidebar Max Width](./devlog/2025-12-17-031000_sidebar_max_width.md): Completed. Constrained the left sidebar in Cube Manager to a maximum width of 400px on large screens to improve layout balance. - [Strict Pack Generation Logic](./devlog/2025-12-17-030600_fix_strict_pack_generation.md): Succeeded. Enforced strict 14-card (Standard) and 13-card (Peasant) limits, removing fallback logic to prevent invalid pack generation. +- [Toast Notification Replacement](./devlog/2025-12-17-022000_replace_alert_with_toast.md): Completed. Replaced invasive alerts with a custom Toast notification system for smoother UX. +- [Enhanced Toast Visibility](./devlog/2025-12-17-023000_enhanced_toast_visibility.md): Completed. Updated toasts to be top-center, animated, and highly visible with premium styling. +- [Unified Toast Design](./devlog/2025-12-17-023500_unified_toast_design.md): Completed. Refined toast aesthetics to match the global dark/slate theme with subtle colored accents. +- [Animated Copy Button](./devlog/2025-12-17-024000_animated_copy_button.md): Completed. Replaced copy toast with an in-place animated tick button for immediate feedback. diff --git a/docs/development/devlog/2025-12-17-022000_replace_alert_with_toast.md b/docs/development/devlog/2025-12-17-022000_replace_alert_with_toast.md new file mode 100644 index 0000000..aa3b88d --- /dev/null +++ b/docs/development/devlog/2025-12-17-022000_replace_alert_with_toast.md @@ -0,0 +1,16 @@ + +### Replaced Alert with Toast Notification + +**Status**: Completed +**Date**: 2025-12-17 + +**Description** +Replaced the invasive `alert()` on the "Copy Pack" button with a non-intrusive Toast notification. + +**Changes** +1. Created `src/client/src/components/Toast.tsx` with a `ToastProvider` and `useToast` hook. +2. Wrapped `App.tsx` with `ToastProvider`. +3. Updated `PackCard.tsx` to use `showToast` instead of `alert`. + +**Next Steps** +- Consider replacing other alerts in `CubeManager` with Toasts for consistency. diff --git a/docs/development/devlog/2025-12-17-023000_enhanced_toast_visibility.md b/docs/development/devlog/2025-12-17-023000_enhanced_toast_visibility.md new file mode 100644 index 0000000..047c16e --- /dev/null +++ b/docs/development/devlog/2025-12-17-023000_enhanced_toast_visibility.md @@ -0,0 +1,21 @@ + +### Enhanced Toast Visibility + +**Status**: Completed +**Date**: 2025-12-17 + +**Description** +Updated the Toast notification to be more prominent and centrally located, as per user feedback. + +**Changes** +1. **Position**: Moved from bottom-right to top-center (`top-6 left-1/2 -translate-x-1/2`). +2. **Animation**: Changed to `slide-in-from-top-full` with a slight zoom-in effect. +3. **Styling**: + - Increased padding (`px-6 py-4`). + - Increased border width (`border-2`) and brightness. + - Added stronger shadows (`shadow-2xl`). + - Increased contrast for text and background. + - Increased font weight to `bold`. + +**Effect** +Notifications are now impossible to miss, appearing top-center with a premium, game-like alert style. diff --git a/docs/development/devlog/2025-12-17-023500_unified_toast_design.md b/docs/development/devlog/2025-12-17-023500_unified_toast_design.md new file mode 100644 index 0000000..e810a21 --- /dev/null +++ b/docs/development/devlog/2025-12-17-023500_unified_toast_design.md @@ -0,0 +1,17 @@ + +### Unified Toast Design + +**Status**: Completed +**Date**: 2025-12-17 + +**Description** +Refined the Toast notification design to align perfectly with the "Dark Gaming Aesthetic" of the platform. + +**Changes** +1. **Consistent Palette**: Switched to `bg-slate-800` (cards) with `border-slate-700` equivalents, using colored accents only for borders and icons. +2. **Icon Enclosure**: Icons are now housed in a circular, semi-transparent colored background (`bg-emerald-500/10`) for a polished look. +3. **Typography**: Reverted to standard font weights used in other UI cards (`font-medium`) for better readability and consistency. +4. **Shadows**: Tuned shadows to be deep but subtle (`shadow-2xl shadow-emerald-900/20`), matching the ambient lighting of the app. + +**Effect** +The Toast now feels like a native part of the UI rather than a generic alert overlay. diff --git a/docs/development/devlog/2025-12-17-024000_animated_copy_button.md b/docs/development/devlog/2025-12-17-024000_animated_copy_button.md new file mode 100644 index 0000000..55f2e95 --- /dev/null +++ b/docs/development/devlog/2025-12-17-024000_animated_copy_button.md @@ -0,0 +1,20 @@ + +### Animated Copy Button + +**Status**: Completed +**Date**: 2025-12-17 + +**Description** +Replaced the toast notification for the copy action with a self-contained, animated button state. + +**Changes** +1. **Removed Toast Usage**: Detached `useToast` from `PackCard.tsx`. +2. **Local State**: Implemented `copied` state in `PackCard`. +3. **UI Feedback**: + - Button transitions from "Copy" (slate) to "Check" (emerald/green) on click. + - Added `animate-in zoom-in spin-in-12` for a satisfying "tick" animation. + - Button background and border glow green to confirm success. + - Auto-reverts after 2 seconds. + +**Effect** +Provides immediate, localized feedback for the copy action without clogging the global UI with notifications. diff --git a/src/client/src/App.tsx b/src/client/src/App.tsx index 4827ddc..da1a2d9 100644 --- a/src/client/src/App.tsx +++ b/src/client/src/App.tsx @@ -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 ( -
-
-
-
-
-
-

MTG Peasant Drafter

-

Pack Generator & Tournament Manager

+ +
+
+
+
+
+
+

MTG Peasant Drafter

+

Pack Generator & Tournament Manager

+
+
+ +
+ + + +
+
-
- - - - -
-
-
- -
- {activeTab === 'draft' && ( - setActiveTab('lobby')} - /> - )} - {activeTab === 'lobby' && } - {activeTab === 'tester' && } - {activeTab === 'bracket' && } -
-
+
+ {activeTab === 'draft' && ( + setActiveTab('lobby')} + /> + )} + {activeTab === 'lobby' && } + {activeTab === 'tester' && } + {activeTab === 'bracket' && } +
+ + ); }; diff --git a/src/client/src/components/PackCard.tsx b/src/client/src/components/PackCard.tsx index 9ebfb66..f6fc830 100644 --- a/src/client/src/components/PackCard.tsx +++ b/src/client/src/components/PackCard.tsx @@ -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 = ({ 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 = ({ 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 = ({ pack, viewMode, cardWidth =

Pack #{pack.id}

{pack.setName} - diff --git a/src/client/src/components/Toast.tsx b/src/client/src/components/Toast.tsx new file mode 100644 index 0000000..cecccda --- /dev/null +++ b/src/client/src/components/Toast.tsx @@ -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(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([]); + + 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 ( + + {children} +
+ {toasts.map((toast) => ( +
+
+ {toast.type === 'success' && } + {toast.type === 'error' && } + {toast.type === 'info' && } +
+ +
+ {toast.message} +
+ + +
+ ))} +
+
+ ); +};