@@ -6,6 +6,8 @@ import type { Card, Comparison } from '../../types';
66import { Button } from '../ui/Button' ;
77import { SwipeableCard } from './SwipeableCard' ;
88
9+ type ComparisonMode = 'pick' | 'discard' ;
10+
911interface SwipeComparisonViewProps {
1012 comparison : Comparison ;
1113 onSelect : ( winner : Card ) => void ;
@@ -28,6 +30,7 @@ export function SwipeComparisonView({
2830 const [ isSubmitting , setIsSubmitting ] = useState ( false ) ;
2931 const [ showInstructions , setShowInstructions ] = useState ( true ) ;
3032 const [ focusedCardIndex , setFocusedCardIndex ] = useState < number > ( 0 ) ;
33+ const [ mode , setMode ] = useState < ComparisonMode > ( 'pick' ) ;
3134
3235 // Hide instructions after first interaction
3336 useEffect ( ( ) => {
@@ -38,6 +41,23 @@ export function SwipeComparisonView({
3841 return ( ) => clearTimeout ( timer ) ;
3942 } , [ ] ) ;
4043
44+ // Helper function to execute the current mode action
45+ const executeAction = ( card : Card ) => {
46+ if ( mode === 'pick' ) {
47+ handleSelection ( card ) ;
48+ } else {
49+ // In discard mode, select the other card(s)
50+ const otherCards = comparison . cards . filter ( c => c . id !== card . id ) ;
51+ if ( otherCards . length === 1 ) {
52+ handleSelection ( otherCards [ 0 ] ) ;
53+ } else if ( otherCards . length > 1 ) {
54+ // For multi-card comparisons in discard mode, we'd need more complex logic
55+ // For now, just pick the first non-discarded card
56+ handleSelection ( otherCards [ 0 ] ) ;
57+ }
58+ }
59+ } ;
60+
4161 // Keyboard navigation
4262 useEffect ( ( ) => {
4363 const handleKeyDown = ( event : KeyboardEvent ) => {
@@ -46,45 +66,76 @@ export function SwipeComparisonView({
4666 switch ( event . key ) {
4767 case 'ArrowLeft' :
4868 event . preventDefault ( ) ;
49- setFocusedCardIndex ( 0 ) ;
50- setSelectedCard ( comparison . cards [ 0 ] ) ;
69+ if ( comparison . cards . length === 2 ) {
70+ // With 2 cards: left arrow performs current action on left card
71+ executeAction ( comparison . cards [ 0 ] ) ;
72+ } else {
73+ // With more cards: left arrow navigates to previous card
74+ setFocusedCardIndex (
75+ prev => ( prev - 1 + comparison . cards . length ) % comparison . cards . length
76+ ) ;
77+ }
5178 break ;
5279 case 'ArrowRight' :
5380 event . preventDefault ( ) ;
54- setFocusedCardIndex ( 1 ) ;
55- setSelectedCard ( comparison . cards [ 1 ] ) ;
81+ if ( comparison . cards . length === 2 ) {
82+ // With 2 cards: right arrow performs current action on right card
83+ executeAction ( comparison . cards [ 1 ] ) ;
84+ } else {
85+ // With more cards: right arrow navigates to next card
86+ setFocusedCardIndex ( prev => ( prev + 1 ) % comparison . cards . length ) ;
87+ }
88+ break ;
89+ case ' ' :
90+ event . preventDefault ( ) ;
91+ if ( comparison . cards . length > 2 ) {
92+ // With more cards: space performs action on focused card
93+ executeAction ( comparison . cards [ focusedCardIndex ] ) ;
94+ } else {
95+ // With 2 cards: space cancels selection
96+ setSelectedCard ( null ) ;
97+ }
5698 break ;
5799 case 'Enter' :
58100 event . preventDefault ( ) ;
59101 if ( selectedCard ) {
60102 handleSelection ( selectedCard ) ;
61103 } else if ( comparison . cards [ focusedCardIndex ] ) {
62- handleSelection ( comparison . cards [ focusedCardIndex ] ) ;
104+ executeAction ( comparison . cards [ focusedCardIndex ] ) ;
63105 }
64106 break ;
65- case ' ' :
66107 case 'Escape' :
67108 event . preventDefault ( ) ;
68109 setSelectedCard ( null ) ;
69110 break ;
111+ case 'p' :
112+ case 'P' :
113+ event . preventDefault ( ) ;
114+ setMode ( 'pick' ) ;
115+ break ;
116+ case 'd' :
117+ case 'D' :
118+ event . preventDefault ( ) ;
119+ setMode ( 'discard' ) ;
120+ break ;
70121 case '1' :
71122 event . preventDefault ( ) ;
72123 if ( comparison . cards [ 0 ] ) {
73- handleSelection ( comparison . cards [ 0 ] ) ;
124+ executeAction ( comparison . cards [ 0 ] ) ;
74125 }
75126 break ;
76127 case '2' :
77128 event . preventDefault ( ) ;
78129 if ( comparison . cards [ 1 ] ) {
79- handleSelection ( comparison . cards [ 1 ] ) ;
130+ executeAction ( comparison . cards [ 1 ] ) ;
80131 }
81132 break ;
82133 }
83134 } ;
84135
85136 document . addEventListener ( 'keydown' , handleKeyDown ) ;
86137 return ( ) => document . removeEventListener ( 'keydown' , handleKeyDown ) ;
87- } , [ disabled , isSubmitting , selectedCard , focusedCardIndex , comparison . cards ] ) ;
138+ } , [ disabled , isSubmitting , selectedCard , focusedCardIndex , comparison . cards , mode ] ) ;
88139
89140 const handleCardSwipeRight = ( card : Card ) => {
90141 if ( disabled || isSubmitting ) return ;
@@ -152,6 +203,36 @@ export function SwipeComparisonView({
152203 </ div >
153204 ) }
154205
206+ { /* Mode toggle */ }
207+ < div className = "flex items-center justify-center" >
208+ < div className = "bg-muted p-1 rounded-lg flex gap-1" >
209+ < button
210+ type = "button"
211+ onClick = { ( ) => setMode ( 'pick' ) }
212+ className = { `px-4 py-2 text-sm font-medium rounded-md transition-all ${
213+ mode === 'pick'
214+ ? 'bg-primary text-primary-foreground shadow-sm'
215+ : 'text-muted-foreground hover:text-foreground hover:bg-background/50'
216+ } `}
217+ aria-pressed = { mode === 'pick' }
218+ >
219+ Pick Mode
220+ </ button >
221+ < button
222+ type = "button"
223+ onClick = { ( ) => setMode ( 'discard' ) }
224+ className = { `px-4 py-2 text-sm font-medium rounded-md transition-all ${
225+ mode === 'discard'
226+ ? 'bg-primary text-primary-foreground shadow-sm'
227+ : 'text-muted-foreground hover:text-foreground hover:bg-background/50'
228+ } `}
229+ aria-pressed = { mode === 'discard' }
230+ >
231+ Discard Mode
232+ </ button >
233+ </ div >
234+ </ div >
235+
155236 { /* Instructions */ }
156237 < AnimatePresence >
157238 { showInstructions && (
@@ -162,9 +243,37 @@ export function SwipeComparisonView({
162243 exit = { { opacity : 0 , y : - 20 } }
163244 transition = { { duration : 0.3 } }
164245 >
165- < h2 className = "text-2xl font-semibold" > Which do you prefer?</ h2 >
246+ < h2 className = "text-2xl font-semibold" >
247+ { mode === 'pick' ? 'Which do you prefer?' : 'Which do you want to discard?' }
248+ </ h2 >
166249 < p className = "text-muted-foreground" >
167- Swipe right to choose, swipe left to prefer the other, or tap to select manually
250+ { comparison . cards . length === 2 ? (
251+ mode === 'pick' ? (
252+ < >
253+ Use < kbd className = "px-1 py-0.5 bg-muted rounded text-xs" > ←</ kbd > { ' ' }
254+ < kbd className = "px-1 py-0.5 bg-muted rounded text-xs" > →</ kbd > to pick, or tap to
255+ select manually
256+ </ >
257+ ) : (
258+ < >
259+ Use < kbd className = "px-1 py-0.5 bg-muted rounded text-xs" > ←</ kbd > { ' ' }
260+ < kbd className = "px-1 py-0.5 bg-muted rounded text-xs" > →</ kbd > to discard, or tap
261+ to select manually
262+ </ >
263+ )
264+ ) : mode === 'pick' ? (
265+ < >
266+ Use < kbd className = "px-1 py-0.5 bg-muted rounded text-xs" > ←</ kbd > { ' ' }
267+ < kbd className = "px-1 py-0.5 bg-muted rounded text-xs" > →</ kbd > to navigate,{ ' ' }
268+ < kbd className = "px-1 py-0.5 bg-muted rounded text-xs" > Space</ kbd > to pick
269+ </ >
270+ ) : (
271+ < >
272+ Use < kbd className = "px-1 py-0.5 bg-muted rounded text-xs" > ←</ kbd > { ' ' }
273+ < kbd className = "px-1 py-0.5 bg-muted rounded text-xs" > →</ kbd > to navigate,{ ' ' }
274+ < kbd className = "px-1 py-0.5 bg-muted rounded text-xs" > Space</ kbd > to discard
275+ </ >
276+ ) }
168277 </ p >
169278 </ motion . div >
170279 ) }
@@ -184,6 +293,7 @@ export function SwipeComparisonView({
184293 type : 'spring' ,
185294 stiffness : 100 ,
186295 } }
296+ className = "relative"
187297 >
188298 < button
189299 type = "button"
@@ -192,7 +302,9 @@ export function SwipeComparisonView({
192302 onClick = { ( ) => handleCardTap ( card ) }
193303 onFocus = { ( ) => setFocusedCardIndex ( index ) }
194304 className = { `w-full p-0 border-0 bg-transparent focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 rounded-lg ${
195- focusedCardIndex === index ? 'ring-2 ring-primary/50' : ''
305+ focusedCardIndex === index && comparison . cards . length > 2
306+ ? 'ring-2 ring-primary/70'
307+ : ''
196308 } `}
197309 >
198310 < SwipeableCard
@@ -206,6 +318,19 @@ export function SwipeComparisonView({
206318 } ${ focusedCardIndex === index ? 'shadow-lg' : '' } `}
207319 />
208320 </ button >
321+
322+ { /* Focus indicator for multi-card comparisons */ }
323+ { comparison . cards . length > 2 && focusedCardIndex === index && (
324+ < motion . div
325+ className = "absolute top-2 right-2 bg-primary text-primary-foreground px-2 py-1 rounded-md text-xs font-medium shadow-lg"
326+ initial = { { scale : 0 , opacity : 0 } }
327+ animate = { { scale : 1 , opacity : 1 } }
328+ exit = { { scale : 0 , opacity : 0 } }
329+ transition = { { duration : 0.2 } }
330+ >
331+ Press Space to { mode }
332+ </ motion . div >
333+ ) }
209334 </ motion . div >
210335 ) ) }
211336 </ fieldset >
@@ -275,8 +400,15 @@ export function SwipeComparisonView({
275400 transition = { { delay : 1 } }
276401 aria-label = "Keyboard shortcuts"
277402 >
278- < div > Keyboard shortcuts: ← → arrows to select • Enter to confirm • Space/Esc to cancel</ div >
279- < div > Quick select: 1 for left option • 2 for right option</ div >
403+ < div >
404+ { comparison . cards . length === 2
405+ ? `${ mode === 'pick' ? 'Pick' : 'Discard' } mode: ← → arrows to ${ mode } • P/D to switch mode • Enter to confirm • Esc to cancel`
406+ : `${ mode === 'pick' ? 'Pick' : 'Discard' } mode: ← → to navigate • Space to ${ mode } • P/D to switch mode • Enter to confirm` }
407+ </ div >
408+ < div >
409+ Quick select: 1 for left option • 2 for right option • P for pick mode • D for discard
410+ mode
411+ </ div >
280412 </ motion . section >
281413
282414 { /* Loading overlay */ }
0 commit comments