summaryrefslogtreecommitdiff
path: root/app/routes/favorites.tsx
blob: e2f620d8e8fb968ef119e08cdc0c5dc41c6a2c48 (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
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
151
152
153
154
155
156
157
158
159
160
161
import { Form, Link, useLoaderData, useNavigation } from "react-router";
import type { Route } from "./+types/favorites";
import {
  addFavorite,
  deleteFavorite,
  deletePasskey,
  listFavorites,
  listPasskeys,
  requireLogin,
} from "~/lib/auth.server";

export async function loader({ request }: Route.LoaderArgs) {
  requireLogin(request);
  return { favorites: listFavorites(), passkeys: listPasskeys() };
}

export async function action({ request }: Route.ActionArgs) {
  requireLogin(request);
  const form = await request.formData();
  switch (form.get("intent")) {
    case "add": {
      const added = addFavorite(String(form.get("term") ?? ""));
      return { error: added ? null : "空欄か、登録済みのワードです" };
    }
    case "delete":
      deleteFavorite(Number(form.get("id")));
      return { error: null };
    case "delete-passkey":
      return {
        error: deletePasskey(String(form.get("id"))) ? null : "最後のパスキーは削除できません",
      };
  }
  return { error: null };
}

export default function Favorites({ actionData }: Route.ComponentProps) {
  const { favorites, passkeys } = useLoaderData<typeof loader>();
  const navigation = useNavigation();
  const adding = navigation.formData?.get("intent") === "add";

  return (
    <div className="min-h-screen bg-gray-950 text-gray-100">
      <header className="border-b border-gray-800 px-4 sm:px-6 py-3 sm:py-4 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-0">
        <Link to="/" className="text-xl font-bold tracking-tight text-white">
          🎸 ライブに行くしかない
        </Link>
        <nav className="flex gap-4 sm:gap-6 text-sm text-gray-400">
          <Link to="/events" className="hover:text-white transition-colors">イベント</Link>
          <Link to="/events/by-date" className="hover:text-white transition-colors">日付別</Link>
          <Link to="/venues" className="hover:text-white transition-colors">会場一覧</Link>
          <Link to="/favorites" className="text-white font-medium">★ お気に入り</Link>
        </nav>
      </header>

      <main className="max-w-2xl mx-auto px-4 py-8 flex flex-col gap-10">
        <section>
          <div className="mb-4 flex items-center justify-between">
            <h1 className="text-2xl font-bold">お気に入り</h1>
            <Link
              to="/events?fav=1"
              className="rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-medium hover:bg-indigo-500 transition-colors"
            >
              該当イベントを見る →
            </Link>
          </div>
          <p className="mb-4 text-xs text-gray-400">
            バンド名などを登録すると、タイトルか出演者に含まれるイベントに ★ が付きます(全角/半角・大文字/小文字は区別しません)。
          </p>

          <Form method="post" className="flex gap-2" key={favorites.length}>
            <input type="hidden" name="intent" value="add" />
            <input
              name="term"
              required
              maxLength={100}
              placeholder="バンド名、イベント名..."
              className="flex-1 rounded-md bg-gray-800 border border-gray-700 px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500"
            />
            <button
              type="submit"
              disabled={adding}
              className="rounded-md bg-gray-700 px-4 py-1.5 text-sm font-medium hover:bg-gray-600 disabled:opacity-50 transition-colors"
            >
              追加
            </button>
          </Form>
          {actionData?.error && <p className="mt-2 text-sm text-red-400">{actionData.error}</p>}

          {favorites.length === 0 ? (
            <p className="mt-6 text-sm text-gray-500">まだ登録されていません</p>
          ) : (
            <ul className="mt-4 flex flex-col gap-1.5">
              {favorites.map((f) => (
                <li
                  key={f.id}
                  className="flex items-center gap-3 rounded-lg px-4 py-2.5 bg-gray-800/40 border border-gray-700/30 text-sm"
                >
                  <Link
                    to={`/events?keyword=${encodeURIComponent(f.term)}`}
                    className="flex-1 min-w-0 truncate hover:text-indigo-300 transition-colors"
                  >
                    {f.term}
                  </Link>
                  <span className={`text-xs tabular-nums ${f.upcoming_count > 0 ? "text-indigo-300" : "text-gray-600"}`}>
                    {f.upcoming_count} 件
                  </span>
                  <Form method="post">
                    <input type="hidden" name="intent" value="delete" />
                    <input type="hidden" name="id" value={f.id} />
                    <button type="submit" className="text-gray-500 hover:text-red-400 transition-colors" title="削除">
                      ✕
                    </button>
                  </Form>
                </li>
              ))}
            </ul>
          )}
        </section>

        <section className="border-t border-gray-800 pt-8">
          <h2 className="mb-3 text-lg font-semibold">パスキー</h2>
          <ul className="flex flex-col gap-1.5">
            {passkeys.map((p) => (
              <li key={p.id} className="flex items-center gap-3 rounded-lg px-4 py-2.5 bg-gray-800/40 border border-gray-700/30 text-sm">
                <span className="flex-1 min-w-0 truncate">{p.label ?? "(名前なし)"}</span>
                <span className="text-xs text-gray-500">
                  {p.last_used_at ? `最終使用 ${p.last_used_at.slice(0, 10)}` : `登録 ${p.created_at.slice(0, 10)}`}
                </span>
                {passkeys.length > 1 && (
                  <Form method="post">
                    <input type="hidden" name="intent" value="delete-passkey" />
                    <input type="hidden" name="id" value={p.id} />
                    <button type="submit" className="text-gray-500 hover:text-red-400 transition-colors" title="削除">
                      ✕
                    </button>
                  </Form>
                )}
              </li>
            ))}
          </ul>
          <div className="mt-4 flex gap-3">
            <Link
              to="/login"
              className="rounded-md border border-gray-700 px-4 py-1.5 text-sm text-gray-300 hover:text-white transition-colors"
            >
              パスキーを追加
            </Link>
            <Form method="post" action="/api/auth/logout">
              <button
                type="submit"
                className="rounded-md border border-gray-700 px-4 py-1.5 text-sm text-gray-400 hover:text-white transition-colors"
              >
                ログアウト
              </button>
            </Form>
          </div>
        </section>
      </main>
    </div>
  );
}