| 60644e7 | | | 1 | "use client"; |
| 60644e7 | | | 2 | |
| 60644e7 | | | 3 | import Link from "next/link"; |
| 60644e7 | | | 4 | import { use, useEffect, useState } from "react"; |
| 79efd41 | | | 5 | import { |
| 79efd41 | | | 6 | repos as reposApi, |
| 79efd41 | | | 7 | orgs as orgsApi, |
| 79efd41 | | | 8 | type Repo, |
| 79efd41 | | | 9 | type Org, |
| 79efd41 | | | 10 | type OrgMember, |
| 79efd41 | | | 11 | } from "@/lib/api"; |
| 0b1c50f | | | 12 | import { timeAgo } from "@/lib/utils"; |
| 60644e7 | | | 13 | import { ListSkeleton } from "@/app/components/skeleton"; |
| 13a9fd1 | | | 14 | import { useAuth } from "@/lib/auth"; |
| 60644e7 | | | 15 | |
| 60644e7 | | | 16 | interface Props { |
| 60644e7 | | | 17 | params: Promise<{ owner: string }>; |
| 60644e7 | | | 18 | } |
| 60644e7 | | | 19 | |
| 79efd41 | | | 20 | export default function OwnerPage({ params }: Props) { |
| 60644e7 | | | 21 | const { owner } = use(params); |
| 13a9fd1 | | | 22 | const { user } = useAuth(); |
| 60644e7 | | | 23 | const [repoList, setRepoList] = useState<Repo[]>([]); |
| 79efd41 | | | 24 | const [orgData, setOrgData] = useState<{ |
| 79efd41 | | | 25 | org: Org; |
| 79efd41 | | | 26 | members: OrgMember[]; |
| 79efd41 | | | 27 | } | null>(null); |
| 79efd41 | | | 28 | const [isOrg, setIsOrg] = useState(false); |
| 60644e7 | | | 29 | const [loaded, setLoaded] = useState(false); |
| 0b1c50f | | | 30 | const [lastCommitTimes, setLastCommitTimes] = useState<Record<number, number>>({}); |
| 60644e7 | | | 31 | |
| 1da9874 | | | 32 | useEffect(() => { |
| 1da9874 | | | 33 | document.title = owner; |
| 1da9874 | | | 34 | }, [owner]); |
| 1da9874 | | | 35 | |
| 60644e7 | | | 36 | useEffect(() => { |
| 79efd41 | | | 37 | Promise.all([ |
| 79efd41 | | | 38 | orgsApi.get(owner).catch(() => null), |
| 79efd41 | | | 39 | reposApi |
| 79efd41 | | | 40 | .list() |
| 79efd41 | | | 41 | .then(({ repos }) => repos.filter((r) => r.owner_name === owner)), |
| 79efd41 | | | 42 | ]) |
| 79efd41 | | | 43 | .then(([orgResult, repos]) => { |
| 79efd41 | | | 44 | if (orgResult) { |
| 79efd41 | | | 45 | setOrgData(orgResult); |
| 79efd41 | | | 46 | setIsOrg(true); |
| 79efd41 | | | 47 | } |
| 79efd41 | | | 48 | setRepoList(repos); |
| 79efd41 | | | 49 | }) |
| 60644e7 | | | 50 | .catch(() => {}) |
| 60644e7 | | | 51 | .finally(() => setLoaded(true)); |
| 60644e7 | | | 52 | }, [owner]); |
| 60644e7 | | | 53 | |
| 0b1c50f | | | 54 | useEffect(() => { |
| 0b1c50f | | | 55 | if (repoList.length === 0) return; |
| 0b1c50f | | | 56 | Promise.all( |
| 0b1c50f | | | 57 | repoList.map(async (repo) => { |
| 0b1c50f | | | 58 | try { |
| 0b1c50f | | | 59 | const data = await reposApi.commits(repo.owner_name, repo.name, "main", { limit: 1 }); |
| 0b1c50f | | | 60 | const latest = data.commits[0]; |
| 0b1c50f | | | 61 | if (latest) return { id: repo.id, timestamp: latest.timestamp }; |
| 0b1c50f | | | 62 | } catch {} |
| 0b1c50f | | | 63 | return null; |
| 0b1c50f | | | 64 | }) |
| 0b1c50f | | | 65 | ).then((results) => { |
| 0b1c50f | | | 66 | const times: Record<number, number> = {}; |
| 0b1c50f | | | 67 | for (const r of results) { |
| 0b1c50f | | | 68 | if (r) times[r.id] = r.timestamp; |
| 0b1c50f | | | 69 | } |
| 0b1c50f | | | 70 | setLastCommitTimes(times); |
| 0b1c50f | | | 71 | }); |
| 0b1c50f | | | 72 | }, [repoList]); |
| 0b1c50f | | | 73 | |
| 60644e7 | | | 74 | return ( |
| 60644e7 | | | 75 | <div className="max-w-3xl mx-auto px-4 py-6"> |
| 79efd41 | | | 76 | <h1 className="text-lg mb-1"> |
| 79efd41 | | | 77 | {isOrg && orgData |
| 79efd41 | | | 78 | ? orgData.org.display_name || orgData.org.name |
| 79efd41 | | | 79 | : owner} |
| 79efd41 | | | 80 | </h1> |
| 79efd41 | | | 81 | {isOrg && orgData && ( |
| 79efd41 | | | 82 | <p className="text-xs mb-6" style={{ color: "var(--text-faint)" }}> |
| 79efd41 | | | 83 | {orgData.members.length} member |
| 79efd41 | | | 84 | {orgData.members.length !== 1 ? "s" : ""} |
| 79efd41 | | | 85 | {" \u00b7 "} |
| 79efd41 | | | 86 | {orgData.members.map((m) => m.username).join(", ")} |
| 79efd41 | | | 87 | </p> |
| 79efd41 | | | 88 | )} |
| 79efd41 | | | 89 | |
| 79efd41 | | | 90 | {!isOrg && <div className="mb-6" />} |
| 60644e7 | | | 91 | |
| 13a9fd1 | | | 92 | {user && ( |
| 13a9fd1 | | | 93 | <div className="mb-4"> |
| 13a9fd1 | | | 94 | <Link |
| 13a9fd1 | | | 95 | href={`/new?owner=${encodeURIComponent(owner)}`} |
| 13a9fd1 | | | 96 | className="inline-block px-3 py-1.5 text-sm" |
| 13a9fd1 | | | 97 | style={{ |
| 13a9fd1 | | | 98 | backgroundColor: "var(--accent)", |
| 13a9fd1 | | | 99 | color: "var(--accent-text)", |
| 13a9fd1 | | | 100 | }} |
| 13a9fd1 | | | 101 | > |
| 13a9fd1 | | | 102 | New repository |
| 13a9fd1 | | | 103 | </Link> |
| 13a9fd1 | | | 104 | </div> |
| 13a9fd1 | | | 105 | )} |
| 13a9fd1 | | | 106 | |
| 60644e7 | | | 107 | {!loaded ? ( |
| 60644e7 | | | 108 | <ListSkeleton rows={3} /> |
| 60644e7 | | | 109 | ) : repoList.length === 0 ? ( |
| 60644e7 | | | 110 | <p className="text-sm" style={{ color: "var(--text-muted)" }}> |
| 79efd41 | | | 111 | No repositories found for this {isOrg ? "organization" : "user"}. |
| 60644e7 | | | 112 | </p> |
| 60644e7 | | | 113 | ) : ( |
| 60644e7 | | | 114 | <div |
| 60644e7 | | | 115 | style={{ |
| 60644e7 | | | 116 | border: "1px solid var(--border-subtle)", |
| 60644e7 | | | 117 | }} |
| 60644e7 | | | 118 | > |
| 60644e7 | | | 119 | {repoList.map((repo, i) => ( |
| 60644e7 | | | 120 | <Link |
| 60644e7 | | | 121 | key={repo.id} |
| 60644e7 | | | 122 | href={`/${repo.owner_name}/${repo.name}`} |
| 60644e7 | | | 123 | className="flex items-baseline justify-between py-2.5 px-3 text-sm hover-row" |
| 60644e7 | | | 124 | style={{ |
| 60644e7 | | | 125 | borderTop: i > 0 ? "1px solid var(--divide)" : undefined, |
| 60644e7 | | | 126 | }} |
| 60644e7 | | | 127 | > |
| 8a2c7d4 | | | 128 | <div className="min-w-0 truncate"> |
| 60644e7 | | | 129 | <span style={{ color: "var(--accent)" }}>{repo.name}</span> |
| 60644e7 | | | 130 | {repo.description && ( |
| 60644e7 | | | 131 | <span |
| 8a2c7d4 | | | 132 | className="ml-3 text-xs hidden sm:inline" |
| 60644e7 | | | 133 | style={{ color: "var(--text-faint)" }} |
| 60644e7 | | | 134 | > |
| 60644e7 | | | 135 | {repo.description} |
| 60644e7 | | | 136 | </span> |
| 60644e7 | | | 137 | )} |
| 60644e7 | | | 138 | </div> |
| 0b1c50f | | | 139 | {lastCommitTimes[repo.id] && ( |
| 0b1c50f | | | 140 | <span |
| 0b1c50f | | | 141 | className="text-xs shrink-0 ml-4" |
| 0b1c50f | | | 142 | style={{ color: "var(--text-faint)" }} |
| 0b1c50f | | | 143 | > |
| 0b1c50f | | | 144 | {timeAgo(lastCommitTimes[repo.id])} |
| 0b1c50f | | | 145 | </span> |
| 0b1c50f | | | 146 | )} |
| 60644e7 | | | 147 | </Link> |
| 60644e7 | | | 148 | ))} |
| 60644e7 | | | 149 | </div> |
| 60644e7 | | | 150 | )} |
| 60644e7 | | | 151 | </div> |
| 60644e7 | | | 152 | ); |
| 60644e7 | | | 153 | } |