From 874749df46cc291ca61f2f98092ccabfdeffbf16 Mon Sep 17 00:00:00 2001 From: yyamashita Date: Sat, 26 Sep 2026 00:19:53 +0900 Subject: Add passkey login and favorite search terms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single-user auth: any registered passkey logs in as the owner. First registration needs a one-time token that the app writes to data/registration-token when no passkey exists yet; once logged in, more passkeys can be added without a token. Logged in, /favorites manages a list of terms (e.g. band names). Events whose title or artist contains a term (NFKC + case-insensitive) get a ★, and the event list gains an "お気に入りのみ" filter. Co-Authored-By: Claude Opus 5.5 --- app/routes/favorites.tsx | 161 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 app/routes/favorites.tsx (limited to 'app/routes/favorites.tsx') diff --git a/app/routes/favorites.tsx b/app/routes/favorites.tsx new file mode 100644 index 0000000..e2f620d --- /dev/null +++ b/app/routes/favorites.tsx @@ -0,0 +1,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(); + const navigation = useNavigation(); + const adding = navigation.formData?.get("intent") === "add"; + + return ( +
+
+ + 🎸 ライブに行くしかない + + +
+ +
+
+
+

お気に入り

+ + 該当イベントを見る → + +
+

+ バンド名などを登録すると、タイトルか出演者に含まれるイベントに ★ が付きます(全角/半角・大文字/小文字は区別しません)。 +

+ +
+ + + +
+ {actionData?.error &&

{actionData.error}

} + + {favorites.length === 0 ? ( +

まだ登録されていません

+ ) : ( +
    + {favorites.map((f) => ( +
  • + + {f.term} + + 0 ? "text-indigo-300" : "text-gray-600"}`}> + {f.upcoming_count} 件 + +
    + + + +
    +
  • + ))} +
+ )} +
+ +
+

パスキー

+
    + {passkeys.map((p) => ( +
  • + {p.label ?? "(名前なし)"} + + {p.last_used_at ? `最終使用 ${p.last_used_at.slice(0, 10)}` : `登録 ${p.created_at.slice(0, 10)}`} + + {passkeys.length > 1 && ( +
    + + + +
    + )} +
  • + ))} +
+
+ + パスキーを追加 + +
+ +
+
+
+
+
+ ); +} -- cgit v1.3