SessionItem.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import { isDefaultTitle } from "../../state/SessionContext"
  2. import { formatTimestamp } from "./utils"
  3. import { ideBridge } from "../../lib/ideBridge"
  4. interface SessionItemProps {
  5. session: {
  6. id: string
  7. title: string | null
  8. share?: {
  9. url: string
  10. }
  11. time: {
  12. created: number
  13. }
  14. }
  15. isActive: boolean
  16. isEditing: boolean
  17. isSelectMode: boolean
  18. isSelected: boolean
  19. selectedSessionIndex: number
  20. currentIndex: number
  21. editingTitle: string
  22. editInputRef: React.RefObject<HTMLInputElement | null>
  23. selectedSessionRef: React.RefObject<HTMLDivElement | null>
  24. isSharing: boolean
  25. onSelect: () => void
  26. onEditStart: (e: React.MouseEvent) => void
  27. onEditSave: () => void
  28. onEditCancel: () => void
  29. onEditChange: (value: string) => void
  30. onDeleteStart: (e: React.MouseEvent) => void
  31. onCheckboxChange: (checked: boolean) => void
  32. onKeyDown: (e: React.KeyboardEvent) => void
  33. onToggleShare: (e: React.MouseEvent) => void
  34. }
  35. export function SessionItem({
  36. session,
  37. isActive,
  38. isEditing,
  39. isSelectMode,
  40. isSelected,
  41. selectedSessionIndex,
  42. currentIndex,
  43. editingTitle,
  44. editInputRef,
  45. selectedSessionRef,
  46. isSharing,
  47. onSelect,
  48. onEditStart,
  49. onEditSave,
  50. onEditCancel,
  51. onEditChange,
  52. onDeleteStart,
  53. onCheckboxChange,
  54. onKeyDown,
  55. onToggleShare,
  56. }: SessionItemProps) {
  57. const displayTitle = session.title || "Untitled"
  58. const hasDefaultTitle = isDefaultTitle(displayTitle)
  59. const isShared = !!session.share?.url
  60. const handleLinkClick = (e: React.MouseEvent) => {
  61. e.stopPropagation()
  62. if (session.share?.url) {
  63. if (ideBridge.isInstalled()) {
  64. ideBridge.send({ type: "openUrl", payload: { url: session.share.url } })
  65. } else {
  66. window.open(session.share.url, "_blank", "noopener,noreferrer")
  67. }
  68. }
  69. }
  70. return (
  71. <div
  72. ref={currentIndex === selectedSessionIndex ? selectedSessionRef : null}
  73. tabIndex={-1}
  74. className={`group px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 flex items-center justify-between outline-none ${
  75. currentIndex === selectedSessionIndex && !isSelectMode
  76. ? "bg-blue-50 dark:bg-blue-950"
  77. : isActive
  78. ? "bg-blue-50 dark:bg-blue-950"
  79. : ""
  80. }`}
  81. onClick={() => !isEditing && !isSelectMode && onSelect()}
  82. onKeyDown={onKeyDown}
  83. >
  84. {isEditing ? (
  85. <input
  86. ref={editInputRef}
  87. type="text"
  88. value={editingTitle}
  89. onChange={(e) => onEditChange(e.target.value)}
  90. onKeyDown={(e) => {
  91. if (e.key === "Enter") {
  92. onEditSave()
  93. } else if (e.key === "Escape") {
  94. onEditCancel()
  95. }
  96. }}
  97. onBlur={onEditSave}
  98. onClick={(e) => e.stopPropagation()}
  99. className="flex-1 px-1 py-0.5 text-sm bg-white dark:bg-gray-950 border border-blue-500 rounded outline-none text-gray-900 dark:text-gray-100"
  100. />
  101. ) : (
  102. <>
  103. <div className="flex items-center gap-2 flex-1 min-w-0">
  104. {/* Checkbox for selection mode */}
  105. {isSelectMode && (
  106. <input
  107. type="checkbox"
  108. checked={isSelected}
  109. onChange={(e) => onCheckboxChange(e.target.checked)}
  110. onClick={(e) => e.stopPropagation()}
  111. className="w-3 h-3 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
  112. />
  113. )}
  114. {isActive && !isSelectMode && (
  115. <svg
  116. className="w-3 h-3 text-blue-600 dark:text-blue-400 flex-shrink-0"
  117. fill="currentColor"
  118. viewBox="0 0 20 20"
  119. >
  120. <path
  121. fillRule="evenodd"
  122. d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
  123. clipRule="evenodd"
  124. />
  125. </svg>
  126. )}
  127. <span
  128. className={`truncate ${
  129. hasDefaultTitle
  130. ? "text-gray-500 dark:text-gray-500 italic"
  131. : isActive && !isSelectMode
  132. ? "text-blue-900 dark:text-blue-100 font-medium"
  133. : "text-gray-700 dark:text-gray-300"
  134. }`}
  135. >
  136. {displayTitle}
  137. </span>
  138. </div>
  139. {/* Edit and Delete buttons (hidden in select mode) */}
  140. {!isSelectMode && (
  141. <div className="ml-auto flex items-center gap-2 flex-shrink-0">
  142. {/* Timestamp (hidden on hover or when active) */}
  143. <span
  144. className={`text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap ${isActive ? "hidden" : "block group-hover:hidden"}`}
  145. >
  146. {formatTimestamp(session.time.created)}
  147. </span>
  148. {/* Action buttons (visible on hover or when active) */}
  149. <div className={`${isActive ? "flex" : "hidden group-hover:flex"} items-center gap-1`}>
  150. {/* Link button (only shown if shared) */}
  151. {isShared && (
  152. <button
  153. onClick={handleLinkClick}
  154. className="p-1 text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400"
  155. title="Open share link"
  156. data-tip="Open share link"
  157. >
  158. <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  159. <path
  160. strokeLinecap="round"
  161. strokeLinejoin="round"
  162. strokeWidth={2}
  163. d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
  164. />
  165. </svg>
  166. </button>
  167. )}
  168. {/* Share/Unshare button */}
  169. <button
  170. onClick={onToggleShare}
  171. disabled={isSharing}
  172. className="p-1 text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 disabled:opacity-50"
  173. title={isShared ? "Unshare session" : "Share session"}
  174. data-tip={isShared ? "Unshare session" : "Share session"}
  175. >
  176. {isShared ? (
  177. <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  178. <path
  179. strokeLinecap="round"
  180. strokeLinejoin="round"
  181. strokeWidth={2}
  182. d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"
  183. />
  184. </svg>
  185. ) : (
  186. <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  187. <path
  188. strokeLinecap="round"
  189. strokeLinejoin="round"
  190. strokeWidth={2}
  191. d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"
  192. />
  193. </svg>
  194. )}
  195. </button>
  196. {/* Edit button */}
  197. <button
  198. onClick={onEditStart}
  199. className="p-1 text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400"
  200. title="Edit title"
  201. data-tip="Edit title"
  202. >
  203. <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  204. <path
  205. strokeLinecap="round"
  206. strokeLinejoin="round"
  207. strokeWidth={2}
  208. d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
  209. />
  210. </svg>
  211. </button>
  212. {/* Delete button */}
  213. <button
  214. onClick={onDeleteStart}
  215. className="p-1 text-gray-500 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400"
  216. title="Delete session"
  217. data-tip="Delete session"
  218. >
  219. <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  220. <path
  221. strokeLinecap="round"
  222. strokeLinejoin="round"
  223. strokeWidth={2}
  224. d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
  225. />
  226. </svg>
  227. </button>
  228. </div>
  229. </div>
  230. )}
  231. </>
  232. )}
  233. </div>
  234. )
  235. }