import { data } from "react-router"; import type { Route } from "./+types/users.$username"; import { useTranslation } from "react-i18next"; import { ClientDate } from "~/components/ClientDate"; import { FollowButton } from "~/components/FollowButton"; import { SportBadge } from "~/components/SportBadge"; import { loadUserProfile } from "./users.$username.server"; import { federationEnabled, wantsActivityJson, handleFederationRequest, } from "~/lib/federation.server"; // Content negotiation: this URL is both the human profile (HTML) and the // ActivityPub actor IRI (application/activity+json). AP clients are // short-circuited to Fedify before the HTML loader runs; browsers fall // through untouched. export const middleware: Route.MiddlewareFunction[] = [ async ({ request }, next) => { if (federationEnabled() && wantsActivityJson(request)) { return handleFederationRequest(request); } return next(); }, ]; export async function loader({ params, request }: Route.LoaderArgs) { return data(await loadUserProfile(request, params.username)); } export function meta({ data: loaderData }: Route.MetaArgs) { const user = (loaderData as { user?: { username: string; displayName: string | null; domain: string; bio: string | null } })?.user; const displayName = user?.displayName ?? user?.username ?? "Profile"; const title = `${displayName} (@${user?.username}) — trails.cool`; const description = user?.bio && user.bio.length > 0 ? user.bio.slice(0, 280) : `${displayName} on trails.cool`; if (!user) return [{ title }]; return [ { title }, { property: "og:title", content: title }, { property: "og:description", content: description }, { property: "og:type", content: "profile" }, { property: "og:site_name", content: "trails.cool" }, { name: "twitter:card", content: "summary" }, { name: "twitter:title", content: title }, { name: "twitter:description", content: description }, ]; } export default function UserProfilePage({ loaderData }: Route.ComponentProps) { const { user, routes, activities, activitySort, isOwn, isDemoUser, followers, following, followState, isLoggedIn, canSeeContent } = loaderData; const { t } = useTranslation("journal"); return (
{user.username[0]?.toUpperCase()}

{user.displayName ?? user.username}

{loaderData.profileVisibility === "private" && !isOwn && ( 🔒 {t("profile.lockedBadge")} )} {isDemoUser && ( {t("demo.badge")} )}

@{user.username}@{user.domain}

{user.bio &&

{user.bio}

}
{canSeeContent ? ( {followers}{" "} {t("social.followers.label")} ) : ( {followers}{" "} {t("social.followers.label")} )} {canSeeContent ? ( {following}{" "} {t("social.following.label")} ) : ( {following}{" "} {t("social.following.label")} )}
{!isOwn && isLoggedIn && ( )}
{!canSeeContent && (

{t("profile.privateStub.heading")}

{isLoggedIn ? t("profile.privateStub.bodyAuth") : t("profile.privateStub.bodyAnon")}

{!isLoggedIn && ( {t("auth.login")} )}
)} {isOwn && loaderData.profileVisibility === "private" && (
{t("profile.privateNote")}{" "} {t("profile.goToSettings")}
)} {isOwn && loaderData.profileVisibility === "public" && (
{t("profile.ownNote")}{" "} {t("profile.goToSettings")}
)} {canSeeContent && ( <>

{t("routes.title")} ({routes.length})

{routes.length === 0 ? (

{t("profile.noPublicRoutes")}

) : ( )}

{t("activities.title")} ({activities.length})

{activities.length > 0 && (
{t("activities.sortByDate")} {t("activities.sortByAdded")}
)}
{activities.length === 0 ? (

{t("profile.noPublicActivities")}

) : ( )}
)}
); }