1+ import { useState , useRef } from "react" ;
12import { Card } from "@/components/ui/card" ;
23import { Checkbox } from "@/components/ui/checkbox" ;
34import { MiniScanner } from "@/components/ui/mini-scanner" ;
@@ -9,6 +10,7 @@ interface SessionCardProps {
910 session : Session ;
1011 isSelected : boolean ;
1112 isActive : boolean ;
13+ manageMode : boolean ;
1214 onSelect : ( sessionID : string ) => void ;
1315 onToggleSelection : ( selected : boolean ) => void ;
1416 onDelete : ( e : React . MouseEvent < HTMLButtonElement > ) => void ;
@@ -18,60 +20,159 @@ export const SessionCard = ({
1820 session,
1921 isSelected,
2022 isActive,
23+ manageMode,
2124 onSelect,
2225 onToggleSelection,
2326 onDelete,
2427} : SessionCardProps ) => {
28+ const [ swipeOffset , setSwipeOffset ] = useState ( 0 ) ;
29+ const [ isSwipeOpen , setIsSwipeOpen ] = useState ( false ) ;
30+ const touchStartX = useRef < number | null > ( null ) ;
31+ const cardRef = useRef < HTMLDivElement > ( null ) ;
32+
33+ const handleTouchStart = ( e : React . TouchEvent ) => {
34+ touchStartX . current = e . touches [ 0 ] . clientX ;
35+ } ;
36+
37+ const handleTouchMove = ( e : React . TouchEvent ) => {
38+ if ( touchStartX . current === null ) return ;
39+
40+ const currentX = e . touches [ 0 ] . clientX ;
41+ const diff = touchStartX . current - currentX ;
42+
43+ if ( diff > 0 ) {
44+ const newOffset = Math . min ( diff , 80 ) ;
45+ setSwipeOffset ( newOffset ) ;
46+ } else if ( diff < 0 && isSwipeOpen ) {
47+ const newOffset = Math . max ( 0 , 80 + diff ) ;
48+ setSwipeOffset ( newOffset ) ;
49+ }
50+ } ;
51+
52+ const handleTouchEnd = ( ) => {
53+ if ( swipeOffset > 50 ) {
54+ setIsSwipeOpen ( true ) ;
55+ setSwipeOffset ( 80 ) ;
56+ } else if ( swipeOffset < 30 ) {
57+ setIsSwipeOpen ( false ) ;
58+ setSwipeOffset ( 0 ) ;
59+ }
60+ touchStartX . current = null ;
61+ } ;
62+
63+ const closeSwipe = ( ) => {
64+ setSwipeOffset ( 0 ) ;
65+ setIsSwipeOpen ( false ) ;
66+ } ;
67+
68+ const handleDeleteClick = ( e : React . MouseEvent < HTMLButtonElement > ) => {
69+ e . stopPropagation ( ) ;
70+ onDelete ( e ) ;
71+ closeSwipe ( ) ;
72+ } ;
2573
2674 return (
27- < Card
28- className = { `p-3 cursor-pointer transition-all ${
29- isSelected
30- ? "border-blue-500 shadow-lg shadow-blue-900/30 dark:shadow-blue-900/30 bg-accent"
31- : isActive
32- ? "bg-accent border-border"
33- : "bg-card border-border hover:bg-accent hover:border-border"
34- } hover:shadow-lg`}
35- onClick = { ( ) => onSelect ( session . id ) }
36- >
37- < div className = "flex items-start justify-between gap-2" >
38- < div className = "flex items-start gap-2 flex-1 min-w-0" >
39- < div className = "flex flex-col items-center gap-3 flex-shrink-0" >
40- < Checkbox
41- checked = { isSelected }
42- onCheckedChange = { ( checked ) => {
43- onToggleSelection ( checked === true ) ;
44- } }
45- onClick = { ( e ) => {
46- e . stopPropagation ( ) ;
47- } }
48- className = "w-5 h-5 flex-shrink-0"
49- />
50- < MiniScanner sessionID = { session . id } />
51- </ div >
52- < div className = "flex-1 min-w-0" >
53- < div className = "flex items-center gap-2" >
54- < h3 className = "text-base font-semibold text-orange-600 dark:text-orange-400 truncate" >
55- { session . title || "Untitled Session" }
56- </ h3 >
57- </ div >
58- < div className = "flex items-center gap-3 mt-1 text-xs text-muted-foreground" >
59- < span className = "flex items-center gap-1" >
60- < Clock className = "w-3 h-3" />
61- { formatDistanceToNow ( new Date ( session . time . updated ) , {
62- addSuffix : true ,
63- } ) }
64- </ span >
65- </ div >
66- </ div >
67- </ div >
75+ < div className = "relative" onClick = { closeSwipe } >
76+ < div
77+ className = { `absolute top-0.5 right-0 bottom-0.5 w-20 bg-red-600 flex items-center justify-center rounded-r-lg transition-opacity ${
78+ swipeOffset > 20 || isSwipeOpen ? "opacity-100" : "opacity-0"
79+ } `}
80+ >
6881 < button
69- className = "h-6 w-6 p-0 text-foreground hover: text-red-600 dark: hover:text -red-400 bg-transparent border-none cursor-pointer "
70- onClick = { onDelete }
82+ className = "h-full w-full flex items-center justify-center text-white hover:bg -red-700 "
83+ onClick = { handleDeleteClick }
7184 >
72- < Trash2 className = "w-4 h-4 " />
85+ < Trash2 className = "w-5 h-5 " />
7386 </ button >
7487 </ div >
75- </ Card >
88+ < div
89+ ref = { cardRef }
90+ onTouchStart = { handleTouchStart }
91+ onTouchMove = { handleTouchMove }
92+ onTouchEnd = { handleTouchEnd }
93+ style = { { transform : `translateX(-${ swipeOffset } px)` } }
94+ className = "transition-transform"
95+ >
96+ < Card
97+ className = { `p-2 cursor-pointer transition-all overflow-hidden ${
98+ isSwipeOpen
99+ ? "rounded-none"
100+ : "rounded-r-lg"
101+ } ${
102+ isSelected
103+ ? "border-blue-500 shadow-lg shadow-blue-900/30 dark:shadow-blue-900/30 bg-accent"
104+ : isActive
105+ ? "bg-accent border-border"
106+ : "bg-card border-border hover:bg-accent hover:border-border"
107+ } hover:shadow-lg`}
108+ onClick = { ( ) => {
109+ if ( ! isSwipeOpen ) {
110+ onSelect ( session . id ) ;
111+ }
112+ } }
113+ >
114+ < div className = "flex items-start justify-between gap-2" >
115+ { manageMode ? (
116+ < div className = "flex items-start gap-2 flex-1 min-w-0" >
117+ < div className = "flex flex-col items-center gap-2 flex-shrink-0" >
118+ < Checkbox
119+ checked = { isSelected }
120+ onCheckedChange = { ( checked ) => {
121+ onToggleSelection ( checked === true ) ;
122+ } }
123+ onClick = { ( e ) => {
124+ e . stopPropagation ( ) ;
125+ } }
126+ className = "w-5 h-5 flex-shrink-0"
127+ />
128+ < MiniScanner sessionID = { session . id } />
129+ </ div >
130+ < div className = "flex-1 min-w-0" >
131+ < div className = "flex items-center gap-1" >
132+ < h3 className = "text-base font-semibold text-orange-600 dark:text-orange-400 truncate" >
133+ { session . title || "Untitled Session" }
134+ </ h3 >
135+ </ div >
136+ < div className = "flex items-center gap-2 mt-0.5 text-xs text-muted-foreground" >
137+ < span className = "flex items-center gap-1" >
138+ < Clock className = "w-3 h-3" />
139+ { formatDistanceToNow ( new Date ( session . time . updated ) , {
140+ addSuffix : true ,
141+ } ) }
142+ </ span >
143+ </ div >
144+ </ div >
145+ </ div >
146+ ) : (
147+ < div className = "flex flex-col flex-1 min-w-0" >
148+ < h3 className = "text-sm font-semibold text-orange-600 dark:text-orange-400 truncate" >
149+ { session . title || "Untitled Session" }
150+ </ h3 >
151+ < div className = "flex items-center gap-2 mt-0.5 text-xs text-muted-foreground" >
152+ < span className = "flex items-center" >
153+ < Clock className = "w-3 h-3 mr-1" />
154+ { formatDistanceToNow ( new Date ( session . time . updated ) , {
155+ addSuffix : true ,
156+ } ) }
157+ </ span >
158+ < MiniScanner sessionID = { session . id } />
159+ </ div >
160+ </ div >
161+ ) }
162+ { manageMode && (
163+ < button
164+ className = "h-6 w-6 p-0 text-foreground hover:text-red-600 dark:hover:text-red-400 bg-transparent border-none cursor-pointer"
165+ onClick = { ( e ) => {
166+ e . stopPropagation ( ) ;
167+ onDelete ( e ) ;
168+ } }
169+ >
170+ < Trash2 className = "w-4 h-4" />
171+ </ button >
172+ ) }
173+ </ div >
174+ </ Card >
175+ </ div >
176+ </ div >
76177 ) ;
77178} ;
0 commit comments