import { useState, useCallback, useEffect } from "react"; import { data } from "react-router"; import { useTranslation } from "react-i18next"; import type { Route } from "./+types/home"; import { ClientDate } from "~/components/ClientDate"; import { ClientMap } from "~/components/ClientMap"; import { SportBadge } from "~/components/SportBadge"; import { StatRow } from "~/components/StatRow"; import { activityStatItems } from "~/lib/stats"; import { loadHomeData } from "./home.server"; export function meta(_args: Route.MetaArgs) { return [ { title: "trails.cool" }, { name: "description", content: "Federated outdoor journal. Plan routes, record activities, own your data." }, ]; } export async function loader({ request }: Route.LoaderArgs) { return data(await loadHomeData(request)); } export default function Home({ loaderData }: Route.ComponentProps) { const { user, showAddPasskey, plannerUrl, isFlagship, activities } = 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 { 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]); // ---------- Logged-in: personal activity stream ---------- if (user) { return (

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

{t("activities.new")}
{showAddPasskey && !passkeyDone && supportsPasskey === true && (

{t("addPasskeyPrompt")}

{error &&

{error}

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

{t("addPasskeyPrompt")}

{t("auth.passkeyNotSupported")}

)} {passkeyDone && (

{t("passkeyAdded")}

)} {activities.length === 0 ? (

{t("home.dashboardEmpty")}

) : ( )}
); } // ---------- Logged-out: visitor home (hero + marketing + public feed) ---------- return (
{/* Hero. The site name lives in the top banner + nav brand, so the h1 carries the product pitch instead to avoid "trails.cool" triplication on a narrow strip. */}

{t("home.heroTitle")}

{t("home.heroSubtitle")}

{/* Primary auth CTAs β€” the two actions we actually want most visitors to take. */}
{t("auth.register")} {t("auth.login")}
{/* Demoted escape hatch: the Planner is anonymous and useful on its own, but shouldn't compete visually with sign-up. Same line surfaces /explore so first-time visitors have an in-app path to the local user directory before signing up. */}

{t("home.tryPlannerPrefix")} {t("home.tryPlannerLink")} {t("home.tryPlannerSuffix")} {" Β· "} {t("home.exploreLink")}

{/* Marketing blurbs β€” flagship only. Self-hosted instances link out via the "Powered by trails.cool" footer line below the feed. Card styling mirrors the Planner's feature grid (emoji icon, bordered card) so the two home pages feel consistent. */} {isFlagship && (
{[ { key: "planner", icon: "πŸ—ΊοΈ" }, { key: "journal", icon: "πŸ““" }, { key: "federation", icon: "🌐" }, { key: "ownership", icon: "πŸ”“" }, ].map(({ key, icon }) => (

{t(`home.marketing.${key}.title`)}

{t(`home.marketing.${key}.body`)}

))}
)} {/* Public activity feed */}

{t("home.feed.heading")}

{activities.length === 0 ? (

{t("home.feed.empty")}

) : ( )}
{/* Self-hosted instances link back to the flagship for project info. Flagship instances already show the marketing section above. */} {!isFlagship && (

{t("home.poweredBy")}

)}
); }