summaryrefslogtreecommitdiff
path: root/app/components/ScrapeButton.tsx
blob: 75e48b1b04956714e893cbeb085fdeb9df443b62 (plain)
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
import { useState, useEffect, useRef } from "react";
import { useFetcher } from "react-router";

interface ScrapeLog {
  id: number;
  run_id: string;
  venue_id: string;
  venue_name: string;
  status: "running" | "ok" | "error";
  events_saved: number;
  error: string | null;
}

interface StatusData {
  running: boolean;
  results: ScrapeLog[];
}

interface ScrapeStartData {
  run_id: string;
  status: string;
}

export default function ScrapeButton({ venueId }: { venueId?: string }) {
  const triggerFetcher = useFetcher<ScrapeStartData>();
  const statusFetcher = useFetcher<StatusData>();
  const statusFetcherRef = useRef(statusFetcher);
  useEffect(() => { statusFetcherRef.current = statusFetcher; });

  const [runId, setRunId] = useState<string | null>(null);
  const [polling, setPolling] = useState(false);
  const [done, setDone] = useState(false);

  useEffect(() => {
    const data = triggerFetcher.data;
    if (data?.run_id) {
      setRunId(data.run_id);
      setPolling(true);
      setDone(false);
    }
  }, [triggerFetcher.data]);

  useEffect(() => {
    if (!polling || !runId) return;
    const id = setInterval(() => {
      statusFetcherRef.current.load(`/api/scrape-status?run_id=${runId}`);
    }, 2000);
    return () => clearInterval(id);
  }, [polling, runId]);

  useEffect(() => {
    const data = statusFetcher.data;
    if (data && !data.running) {
      setPolling(false);
      setDone(true);
    }
  }, [statusFetcher.data]);

  const results: ScrapeLog[] = statusFetcher.data?.results ?? [];
  const running = statusFetcher.data?.running ?? false;
  const isActive = triggerFetcher.state !== "idle" || polling;
  const okCount = results.filter((r) => r.status === "ok").length;
  const errCount = results.filter((r) => r.status === "error").length;
  const apiUrl = venueId ? `/api/scrape?venue_id=${venueId}` : "/api/scrape";

  return (
    <div className="flex flex-col items-end gap-2">
      <button
        onClick={() => triggerFetcher.load(apiUrl)}
        disabled={isActive}
        className={`rounded-md px-4 py-1.5 text-sm font-medium transition-colors whitespace-nowrap ${
          isActive
            ? "bg-gray-700 text-gray-500 cursor-not-allowed"
            : "bg-indigo-600 text-white hover:bg-indigo-500"
        }`}
      >
        {isActive ? "更新中..." : "情報を更新"}
      </button>

      {(isActive || done) && results.length > 0 && (
        <div className="w-72 rounded-lg bg-gray-800/80 border border-gray-700/60 p-3 text-xs space-y-1">
          <p className="text-gray-400 font-medium mb-2">
            {running
              ? `スクレイプ中... ${okCount + errCount} / ${results.length} 完了`
              : `完了 — ✔ ${okCount}件成功 / ✖ ${errCount}件失敗`}
          </p>
          {results.map((r) => (
            <div key={r.id} className="flex items-center gap-2">
              <span
                className={
                  r.status === "ok"
                    ? "text-emerald-400"
                    : r.status === "error"
                    ? "text-red-400"
                    : "text-yellow-400"
                }
              >
                {r.status === "ok" ? "✔" : r.status === "error" ? "✖" : "⟳"}
              </span>
              <span className="text-gray-300 flex-1 truncate">{r.venue_name}</span>
              {r.status === "ok" && (
                <span className="text-gray-500 shrink-0">{r.events_saved}件</span>
              )}
              {r.status === "error" && (
                <span className="text-red-400 shrink-0" title={r.error ?? ""}>
                  エラー
                </span>
              )}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}