import { useState, useCallback, useEffect } from "react"; import { data } from "react-router"; import { useTranslation } from "react-i18next"; import { eq, count } from "drizzle-orm"; import type { Route } from "./+types/home"; import { getSessionUser } from "~/lib/auth.server"; import { getDb } from "~/lib/db"; import { credentials } from "@trails-cool/db/schema/journal"; export function meta(_args: Route.MetaArgs) { return [ { title: "trails.cool" }, { name: "description", content: "Your outdoor activity journal" }, ]; } export async function loader({ request }: Route.LoaderArgs) { const user = await getSessionUser(request); const url = new URL(request.url); const addPasskeyParam = url.searchParams.get("add-passkey") === "1" && user !== null; // Only show the add-passkey prompt if the user has no passkeys yet let showAddPasskey = false; if (addPasskeyParam && user) { const db = getDb(); const [row] = await db .select({ count: count() }) .from(credentials) .where(eq(credentials.userId, user.id)); showAddPasskey = (row?.count ?? 0) === 0; } return data({ user: user ? { id: user.id, username: user.username, displayName: user.displayName } : null, showAddPasskey, }); } export default function Home({ loaderData }: Route.ComponentProps) { const { user, showAddPasskey } = loaderData; const { t } = useTranslation("journal"); const [addingPasskey, setAddingPasskey] = useState(false); const [passkeyDone, setPasskeyDone] = useState(false); const [error, setError] = useState(null); const [supportsPasskey, setSupportsPasskey] = useState(null); useEffect(() => { if (showAddPasskey) { import("@simplewebauthn/browser").then(({ browserSupportsWebAuthn }) => { setSupportsPasskey(browserSupportsWebAuthn()); }); } }, [showAddPasskey]); const handleAddPasskey = useCallback(async () => { if (!user) return; setAddingPasskey(true); setError(null); try { // Get registration options for existing user const startResp = await fetch("/api/auth/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ step: "add-passkey", userId: user.id }), }); const startData = await startResp.json(); if (startData.error) { setError(startData.error); return; } const { startRegistration } = await import("@simplewebauthn/browser"); const webAuthnResp = await startRegistration(startData.options); const finishResp = await fetch("/api/auth/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ step: "finish-add-passkey", userId: user.id, response: webAuthnResp, challenge: startData.options.challenge, }), }); const finishData = await finishResp.json(); if (finishData.error) { setError(finishData.error); } else { setPasskeyDone(true); } } catch (err) { setError((err as Error).message); } finally { setAddingPasskey(false); } }, [user]); return (

{t("title")}

{t("subtitle")}

{user ? (

{t("welcome")} {user.displayName ?? user.username}

{showAddPasskey && !passkeyDone && supportsPasskey === true && (

{t("addPasskeyPrompt")}

{error &&

{error}

}
)} {showAddPasskey && !passkeyDone && supportsPasskey === false && (

{t("addPasskeyPrompt")}

{t("auth.passkeyNotSupported")}

)} {passkeyDone && (

{t("passkeyAdded")}

)}
) : (
{t("auth.register")} {t("auth.login")}
)}
); }