File size: 5,558 Bytes
30cc31a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// TaskList.tsx — left-rail task list. Polls listTasks every 5s, supports client-side search,
// highlights the active task based on the current pathname.
"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import { ensureDefaultProject, listTasks } from "@/lib/api";
import type { TaskRecord } from "@/lib/types";

function formatRelative(ts: number): string {
  const diff = Math.max(0, Date.now() - ts);
  const s = Math.floor(diff / 1000);
  if (s < 60) return `${s}s ago`;
  const m = Math.floor(s / 60);
  if (m < 60) return `${m}m ago`;
  const h = Math.floor(m / 60);
  if (h < 24) return `${h}h ago`;
  const d = Math.floor(h / 24);
  return `${d}d ago`;
}

export default function TaskList() {
  const pathname = usePathname();
  const [projectId, setProjectId] = useState<string | null>(null);
  const [tasks, setTasks] = useState<TaskRecord[]>([]);
  const [query, setQuery] = useState("");

  // Resolve default project once on mount.
  useEffect(() => {
    let cancelled = false;
    ensureDefaultProject()
      .then((p) => {
        if (!cancelled) setProjectId(p.id);
      })
      .catch(() => {
        /* Swallow — empty state will render. */
      });
    return () => {
      cancelled = true;
    };
  }, []);

  // Poll tasks every 5s while projectId is known and component is mounted.
  useEffect(() => {
    if (!projectId) return;
    let cancelled = false;

    const fetchTasks = async () => {
      try {
        const { tasks: next } = await listTasks(projectId);
        if (!cancelled) setTasks(next);
      } catch {
        /* Keep last-known list on transient failure. */
      }
    };

    fetchTasks();
    const id = setInterval(fetchTasks, 5000);
    return () => {
      cancelled = true;
      clearInterval(id);
    };
  }, [projectId]);

  const filtered = useMemo(() => {
    const q = query.trim().toLowerCase();
    const sorted = [...tasks].sort((a, b) => b.updatedAt - a.updatedAt);
    if (!q) return sorted;
    return sorted.filter((t) => t.title.toLowerCase().includes(q));
  }, [tasks, query]);

  const activeId = useMemo(() => {
    const m = pathname?.match(/\/tasks\/([^/]+)/);
    return m ? m[1] : null;
  }, [pathname]);

  return (
    <div className="flex flex-col min-h-0">
      <div className="px-3 pt-1 pb-2">
        <p className="text-[10px] font-semibold uppercase tracking-widest text-muted-fg px-2 mb-1.5">
          Tasks
        </p>
        <input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Search tasks…"
          className="w-full rounded-lg border border-border bg-white px-3 py-1.5 text-xs text-foreground placeholder:text-muted-fg focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/20"
        />
      </div>

      {filtered.length === 0 ? (
        <div className="px-4 py-6 text-center flex flex-col items-center">
          {/* Beaker / flask illustration */}
          <svg className="w-10 h-10 mb-2" viewBox="0 0 40 40" fill="none" aria-hidden="true">
            {/* Flask body */}
            <path
              d="M16 8 L16 18 L8 34 C7 36 8.5 38 11 38 L29 38 C31.5 38 33 36 32 34 L24 18 L24 8"
              stroke="currentColor" className="text-muted-fg/30" strokeWidth="1.2" strokeLinejoin="round" fill="currentColor" fillOpacity="0"
            />
            {/* Liquid fill */}
            <path
              d="M12.5 28 L16.8 20 L23.2 20 L27.5 28 L30 33 C30.5 34 29.8 35 29 35 L11 35 C10.2 35 9.5 34 10 33 Z"
              fill="currentColor" className="text-accent/[0.08]"
            />
            {/* Flask neck rim */}
            <line x1="14" y1="8" x2="26" y2="8" stroke="currentColor" className="text-muted-fg/30" strokeWidth="1.2" strokeLinecap="round" />
            {/* Bubbles */}
            <circle cx="17" cy="30" r="1.5" fill="currentColor" className="text-accent/20" />
            <circle cx="22" cy="27" r="1" fill="currentColor" className="text-accent/15" />
            <circle cx="20" cy="32" r="1.2" fill="currentColor" className="text-accent/20" />
          </svg>
          <p className="text-[11px] text-muted-fg leading-relaxed">
            No tasks yet — start a chat to create one.
          </p>
        </div>
      ) : (
        <ul className="flex-1 overflow-y-auto px-2 pb-2 space-y-0.5">
          {filtered.map((t) => {
            const isActive = t.id === activeId;
            return (
              <li key={t.id}>
                <Link
                  href={`/projects/${t.projectId}/tasks/${t.id}`}
                  className={`group flex items-center gap-2 rounded-lg px-2.5 py-1.5 transition-colors ${
                    isActive
                      ? "bg-accent/10 text-foreground"
                      : "text-foreground-dim hover:bg-muted hover:text-foreground"
                  }`}
                >
                  <span
                    className={`shrink-0 w-1.5 h-1.5 rounded-full ${
                      isActive ? "bg-accent" : "bg-muted-fg/40 group-hover:bg-accent/60"
                    }`}
                  />
                  <span className="flex-1 min-w-0 text-xs font-medium truncate">{t.title}</span>
                  <span className="shrink-0 text-[10px] text-muted-fg font-mono">
                    {formatRelative(t.updatedAt)}
                  </span>
                </Link>
              </li>
            );
          })}
        </ul>
      )}
    </div>
  );
}