/ src / components / schedules / schedule-card.tsx
schedule-card.tsx
  1  'use client'
  2  
  3  import type { Schedule } from '@/types'
  4  import { useAppStore } from '@/stores/use-app-store'
  5  import { api } from '@/lib/app/api-client'
  6  import { cronToHuman } from '@/lib/schedules/cron-human'
  7  import { AgentAvatar } from '@/components/agents/agent-avatar'
  8  import { InfoChip } from '@/components/ui/info-chip'
  9  import { useNow } from '@/hooks/use-now'
 10  import { isUserCreatedSchedule } from '@/lib/schedules/schedule-origin'
 11  
 12  const STATUS_COLORS: Record<string, string> = {
 13    active: 'text-emerald-400 bg-emerald-400/[0.08]',
 14    paused: 'text-amber-400 bg-amber-400/[0.08]',
 15    completed: 'text-text-3 bg-white/[0.03]',
 16    failed: 'text-red-400 bg-red-400/[0.08]',
 17  }
 18  
 19  function formatNext(ts: number | undefined, now: number | null): string {
 20    if (!ts) return 'Not scheduled'
 21    if (!now) return 'Scheduled'
 22    const d = new Date(ts)
 23    const diff = ts - now
 24    if (diff < 0) return 'Overdue'
 25    if (diff < 60000) return 'In < 1m'
 26    if (diff < 3600000) return `In ${Math.floor(diff / 60000)}m`
 27    if (diff < 86400000) return `In ${Math.floor(diff / 3600000)}h`
 28    return d.toLocaleDateString()
 29  }
 30  
 31  interface Props {
 32    schedule: Schedule
 33    inSidebar?: boolean
 34    index?: number
 35  }
 36  
 37  export function ScheduleCard({ schedule, inSidebar, index = 0 }: Props) {
 38    const now = useNow({ enabled: false })
 39    const setEditingScheduleId = useAppStore((s) => s.setEditingScheduleId)
 40    const setScheduleSheetOpen = useAppStore((s) => s.setScheduleSheetOpen)
 41    const loadSchedules = useAppStore((s) => s.loadSchedules)
 42    const agents = useAppStore((s) => s.agents)
 43  
 44    const handleClick = () => {
 45      setEditingScheduleId(schedule.id)
 46      setScheduleSheetOpen(true)
 47    }
 48  
 49    const handleToggle = async (e: React.MouseEvent) => {
 50      e.stopPropagation()
 51      const newStatus = schedule.status === 'active' ? 'paused' : 'active'
 52      await api('PUT', `/schedules/${schedule.id}`, { status: newStatus })
 53      loadSchedules()
 54    }
 55  
 56    const handleDelete = async (e: React.MouseEvent) => {
 57      e.stopPropagation()
 58      await api('DELETE', `/schedules/${schedule.id}`)
 59      loadSchedules()
 60    }
 61  
 62    const agent = agents[schedule.agentId]
 63    const creatorAgent = schedule.createdByAgentId ? agents[schedule.createdByAgentId] : null
 64    const statusClass = STATUS_COLORS[schedule.status] || STATUS_COLORS.paused
 65    const canToggle = schedule.status === 'active' || schedule.status === 'paused'
 66  
 67    return (
 68      <div
 69        onClick={handleClick}
 70        className="relative py-3.5 px-4 cursor-pointer rounded-[14px]
 71          transition-all duration-200 active:scale-[0.98]
 72          bg-transparent border border-transparent hover:bg-white/[0.02] hover:border-white/[0.03] hover:scale-[1.01]"
 73        style={{
 74          animation: 'spring-in 0.5s var(--ease-spring) both',
 75          animationDelay: `${Math.min(index * 0.05, 0.4)}s`
 76        }}
 77      >
 78        <div className="flex items-center gap-2.5">
 79          <span className="font-display text-[14px] font-600 truncate flex-1 tracking-[-0.01em]">{schedule.name}</span>
 80          <div className="flex items-center gap-2 shrink-0">
 81            {!inSidebar && canToggle && (
 82              <div
 83                onClick={handleToggle}
 84                className={`w-9 h-5 rounded-full transition-all relative cursor-pointer shrink-0
 85                  ${schedule.status === 'active' ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
 86              >
 87                <div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all
 88                  ${schedule.status === 'active' ? 'left-[18px]' : 'left-0.5'}`}
 89                  style={schedule.status === 'active' ? { animation: 'spring-in 0.3s var(--ease-spring)' } : undefined}
 90                />
 91              </div>
 92            )}
 93            <span className={`text-[10px] font-600 uppercase tracking-wider px-2 py-0.5 rounded-[6px] ${statusClass}`}>
 94              {schedule.status}
 95            </span>
 96            {!inSidebar && (
 97              <button
 98                onClick={handleDelete}
 99                className="text-text-3/40 hover:text-red-400 transition-colors p-0.5"
100                title="Delete"
101              >
102                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
103                  <path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
104                </svg>
105              </button>
106            )}
107          </div>
108        </div>
109        <div className="text-[12px] text-text-3/70 mt-1.5 truncate">
110          Runs on {agent?.name || 'Unknown agent'} &middot; {schedule.scheduleType}
111          {!inSidebar && schedule.scheduleType === 'cron' && schedule.cron && (
112            <span className="text-text-3/50 ml-1" title={schedule.cron}>({cronToHuman(schedule.cron)})</span>
113          )}
114          {!inSidebar && schedule.scheduleType === 'interval' && schedule.intervalMs && (
115            <span className="text-text-3/50 ml-1">
116              (every {schedule.intervalMs >= 3600000
117                ? `${Math.round(schedule.intervalMs / 3600000)}h`
118                : `${Math.round(schedule.intervalMs / 60000)}m`})
119            </span>
120          )}
121        </div>
122        <div className="flex flex-wrap items-center gap-1.5 mt-2">
123          {creatorAgent ? (
124            <InfoChip tone="neutral" className="max-w-full">
125              <AgentAvatar
126                seed={creatorAgent.avatarSeed}
127                avatarUrl={creatorAgent.avatarUrl}
128                name={creatorAgent.name}
129                size={14}
130              />
131              <span className="truncate">Created by {creatorAgent.name}</span>
132            </InfoChip>
133          ) : (
134            <InfoChip tone="muted" className="max-w-full">
135              <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
136                <circle cx="12" cy="8" r="4" />
137                <path d="M4 20c1.5-3.5 4.6-5 8-5s6.5 1.5 8 5" />
138              </svg>
139              <span className="truncate">{isUserCreatedSchedule(schedule) ? 'Created manually' : 'Creator unknown'}</span>
140            </InfoChip>
141          )}
142        </div>
143        <div className="text-[11px] text-text-3/60 mt-1">
144          Next: {formatNext(schedule.nextRunAt, now)}
145        </div>
146      </div>
147    )
148  }