import { ReportLink } from "~/components/ReportLink"; import { createFileRoute, Link, redirect } from "@tanstack/react-router"; import { z } from "zod"; import { getAccountPage, setOrgMember, updateOrgSettings, updateProfile } from "~/fns/account"; import { getProfileExtras } from "~/fns/profile"; import { ProBadge } from "~/components/billing/ProBadge"; import { ContributionSection } from "~/components/profile/ContributionSection"; import { getContributions } from "~/fns/contributions"; import { FollowPanel } from "~/components/profile/FollowPanel"; import { useAction } from "~/hooks/use-action"; import { AvatarUploadButton } from "~/components/settings/AvatarUploadButton"; import { RepoList } from "~/components/RepoList"; import { ActivityFeed } from "~/components/ActivityFeed"; import { Avatar } from "~/ui/Avatar"; import { Badge } from "~/ui/Badge"; import { Markdown } from "~/ui/Markdown"; import { PageContainer } from "~/ui/PageContainer"; import { Panel } from "~/ui/Panel"; import { Button } from "~/ui/Button"; import { SelectField, TextField } from "~/ui/Field"; import { ErrorBanner } from "~/ui/Callout"; import { cn } from "~/utils/cn"; import { ExternalLink, TextLink } from "~/ui/TextLink"; import { Heading } from "~/ui/Heading"; import { safeWebsiteUrl } from "~/lib/validation"; import { seo } from "~/utils/seo"; const TABS = [ { id: "repositories", label: "Repositories" }, { id: "stars", label: "Stars" }, { id: "people", label: "People" }, ] as const; export const Route = createFileRoute("/$owner/")({ validateSearch: z.object({ tab: z.enum(["repositories", "stars", "people"]).optional(), year: z.number().int().min(1970).max(9999).optional(), day: z .string() .regex(/^\d{4}-\d{2}-\d{2}$/) .optional(), repoPage: z.number().int().min(1).max(500).optional(), activityPage: z.number().int().min(1).max(500).optional(), }), loaderDeps: ({ search }) => ({ tab: search.tab ?? "repositories", year: search.year ?? null, day: search.day ?? null, repoPage: search.repoPage ?? 1, activityPage: search.activityPage ?? 1, }), loader: async ({ params, deps }) => { const page = await getAccountPage({ data: { name: params.owner, tab: deps.tab } }); if (page.account.name.toLowerCase() !== params.owner.toLowerCase()) { throw redirect({ to: "/$owner", params: { owner: page.account.name }, search: { tab: deps.tab } }); } const [extras, contributions] = await Promise.all([ getProfileExtras({ data: { name: page.account.name } }), deps.tab === "repositories" ? getContributions({ data: { name: page.account.name, year: deps.year, day: deps.day, repoPage: deps.repoPage, activityPage: deps.activityPage, }, }) : null, ]); return { ...page, extras, contributions }; }, head: ({ loaderData, match, params }) => { if (!loaderData) return { meta: [{ title: `${params.owner} · gitfor` }] }; const { account } = loaderData; const name = account.displayName ? `${account.displayName} (${account.name})` : account.name; return { meta: seo({ origin: match.context.publicUrl, path: `/${account.name}`, title: `${name} · gitfor`, description: account.bio ?? (account.kind === "org" ? `The ${account.name} organization on gitfor` : `${account.name} on gitfor`), image: loaderData.ogImage, imageAlt: name, }), }; }, component: AccountPage, }); function MemberForm({ org }: { org: string }) { const action = useAction(setOrgMember); return (
{ event.preventDefault(); const form = new FormData(event.currentTarget); const role = form.get("role"); action.run({ data: { org, username: String(form.get("username") ?? ""), role: role === "owner" ? "owner" : role === "none" ? "none" : "member", }, }); }} title="Users who are not members yet receive an invitation they must accept." >
); } type OrgSettingsValue = { defaultPermission: "none" | "read" | "write"; membersVisibility: "public" | "members" }; function OrgSettingsForm({ org, settings }: { org: string; settings: OrgSettingsValue }) { const action = useAction(updateOrgSettings); return (
{ event.preventDefault(); const form = new FormData(event.currentTarget); const permission = form.get("defaultPermission"); action.run({ data: { org, defaultPermission: permission === "none" ? "none" : permission === "write" ? "write" : "read", membersVisibility: form.get("membersVisibility") === "members" ? "members" : "public", }, }); }} > ); } function ProfileEditor({ name, displayName, bio, website, }: { name: string; displayName: string | null; bio: string | null; website: string | null; }) { const action = useAction(updateProfile); return (
{ event.preventDefault(); const form = new FormData(event.currentTarget); const value = (key: string) => String(form.get(key) ?? "").trim() || null; action.run({ data: { account: name, displayName: value("displayName"), bio: value("bio"), website: value("website") }, }); }} > ); } const ORG_ADMIN_LINKS = [ { to: "/orgs/$org/runners", label: "Runners" }, { to: "/orgs/$org/integrations", label: "Integrations" }, { to: "/orgs/$org/containers", label: "Containers" }, { to: "/orgs/$org/settings", label: "Settings" }, ] as const; /** Org members see where the org's usage goes; owners also get the management pages. */ function OrgManageLinks({ org, canAdmin }: { org: string; canAdmin: boolean }) { const linkClass = "block text-sm text-kumo-subtle hover:text-accent-400"; return ( ); } function AccountPage() { const data = Route.useLoaderData(); const { tab = "repositories" } = Route.useSearch(); const { account, extras } = data; const tabs = TABS.filter((item) => (account.kind === "org" ? item.id !== "stars" : item.id !== "people")); return (
{extras.readmeHtml && tab === "repositories" && ( )} {tab === "repositories" && data.contributions && ( )} {tab === "people" ? (
    {data.members.map((member) => (
  • {member.name} {member.role}
  • ))}
{data.canAdmin && data.invitations.length > 0 && (
    {data.invitations.map((invitation) => (
  • {invitation.name} invited as {invitation.role}
  • ))}
)} {data.canAdmin && } {data.canAdmin && data.orgSettings && }
) : ( )} {account.kind === "user" && tab === "repositories" && (

Recent activity

)}
); }