GroupCard.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. import { useState } from 'react'
  2. import { Folder, MoreVertical, Pencil, Trash2, FolderInput } from 'lucide-react'
  3. import { WorkflowGroup } from '../api/workflowGroups'
  4. import { formatDistanceToNow } from 'date-fns'
  5. interface GroupCardProps {
  6. group: WorkflowGroup
  7. onClick: () => void
  8. onRename: () => void
  9. onMove: () => void
  10. onDelete: () => void
  11. }
  12. // Helper to safely format timestamps. The API layer normalises the database's
  13. // nanosecond, snake_case metadata into milliseconds and hands back null when a
  14. // document carries no usable timestamp.
  15. function safeFormatDistanceToNow(timestamp: number | null | undefined): string {
  16. if (timestamp === null || timestamp === undefined) {
  17. return 'unknown'
  18. }
  19. if (timestamp <= 0 || timestamp > 8640000000000000) {
  20. return 'unknown'
  21. }
  22. try {
  23. return formatDistanceToNow(timestamp, { addSuffix: true })
  24. } catch {
  25. return 'unknown'
  26. }
  27. }
  28. export function GroupCard({ group, onClick, onRename, onMove, onDelete }: GroupCardProps) {
  29. const [showMenu, setShowMenu] = useState(false)
  30. return (
  31. <div className="bg-white dark:bg-slate-800 rounded-xl border border-gray-200 dark:border-slate-700 p-4 hover:shadow-md transition-shadow">
  32. <div className="flex items-start justify-between mb-3">
  33. <button
  34. onClick={onClick}
  35. className="flex-1 flex items-start gap-3 text-left"
  36. >
  37. <div className="p-2 bg-amber-100 dark:bg-amber-900/30 rounded-lg">
  38. <Folder className="w-5 h-5 text-amber-600 dark:text-amber-400" />
  39. </div>
  40. <div className="flex-1 min-w-0">
  41. <h3 className="font-semibold text-gray-900 dark:text-gray-100 hover:text-primary-600 dark:hover:text-primary-400 truncate">
  42. {group.name}
  43. </h3>
  44. {group.description && (
  45. <p className="text-sm text-gray-500 dark:text-gray-400 mt-1 line-clamp-2">
  46. {group.description}
  47. </p>
  48. )}
  49. </div>
  50. </button>
  51. <div className="relative flex-shrink-0">
  52. <button
  53. onClick={(e) => {
  54. e.stopPropagation()
  55. setShowMenu(!showMenu)
  56. }}
  57. className="p-1 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 rounded"
  58. >
  59. <MoreVertical className="w-5 h-5" />
  60. </button>
  61. {showMenu && (
  62. <>
  63. <div
  64. className="fixed inset-0 z-10"
  65. onClick={() => setShowMenu(false)}
  66. />
  67. <div className="absolute right-0 mt-1 w-40 bg-white dark:bg-slate-800 rounded-lg shadow-lg border border-gray-200 dark:border-slate-700 z-20">
  68. <button
  69. onClick={(e) => {
  70. e.stopPropagation()
  71. onRename()
  72. setShowMenu(false)
  73. }}
  74. className="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-700 flex items-center gap-2"
  75. >
  76. <Pencil className="w-4 h-4" /> Rename
  77. </button>
  78. <button
  79. onClick={(e) => {
  80. e.stopPropagation()
  81. onMove()
  82. setShowMenu(false)
  83. }}
  84. className="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-700 flex items-center gap-2"
  85. >
  86. <FolderInput className="w-4 h-4" /> Move
  87. </button>
  88. <button
  89. onClick={(e) => {
  90. e.stopPropagation()
  91. onDelete()
  92. setShowMenu(false)
  93. }}
  94. className="w-full px-4 py-2 text-left text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 flex items-center gap-2"
  95. >
  96. <Trash2 className="w-4 h-4" /> Delete
  97. </button>
  98. </div>
  99. </>
  100. )}
  101. </div>
  102. </div>
  103. <div className="flex items-center justify-end text-sm">
  104. <span className="text-gray-400 dark:text-gray-500">
  105. {safeFormatDistanceToNow(group.updatedAt)}
  106. </span>
  107. </div>
  108. </div>
  109. )
  110. }