web/app/%5Bowner%5D/page.tsxblame
View source
60644e71"use client";
60644e72
60644e73import Link from "next/link";
60644e74import { use, useEffect, useState } from "react";
79efd415import {
79efd416 repos as reposApi,
79efd417 orgs as orgsApi,
79efd418 type Repo,
79efd419 type Org,
79efd4110 type OrgMember,
79efd4111} from "@/lib/api";
0b1c50f12import { timeAgo } from "@/lib/utils";
60644e713import { ListSkeleton } from "@/app/components/skeleton";
13a9fd114import { useAuth } from "@/lib/auth";
60644e715
60644e716interface Props {
60644e717 params: Promise<{ owner: string }>;
60644e718}
60644e719
79efd4120export default function OwnerPage({ params }: Props) {
60644e721 const { owner } = use(params);
13a9fd122 const { user } = useAuth();
60644e723 const [repoList, setRepoList] = useState<Repo[]>([]);
79efd4124 const [orgData, setOrgData] = useState<{
79efd4125 org: Org;
79efd4126 members: OrgMember[];
79efd4127 } | null>(null);
79efd4128 const [isOrg, setIsOrg] = useState(false);
60644e729 const [loaded, setLoaded] = useState(false);
0b1c50f30 const [lastCommitTimes, setLastCommitTimes] = useState<Record<number, number>>({});
60644e731
1da987432 useEffect(() => {
1da987433 document.title = owner;
1da987434 }, [owner]);
1da987435
60644e736 useEffect(() => {
79efd4137 Promise.all([
79efd4138 orgsApi.get(owner).catch(() => null),
79efd4139 reposApi
79efd4140 .list()
79efd4141 .then(({ repos }) => repos.filter((r) => r.owner_name === owner)),
79efd4142 ])
79efd4143 .then(([orgResult, repos]) => {
79efd4144 if (orgResult) {
79efd4145 setOrgData(orgResult);
79efd4146 setIsOrg(true);
79efd4147 }
79efd4148 setRepoList(repos);
79efd4149 })
60644e750 .catch(() => {})
60644e751 .finally(() => setLoaded(true));
60644e752 }, [owner]);
60644e753
0b1c50f54 useEffect(() => {
0b1c50f55 if (repoList.length === 0) return;
0b1c50f56 Promise.all(
0b1c50f57 repoList.map(async (repo) => {
0b1c50f58 try {
0b1c50f59 const data = await reposApi.commits(repo.owner_name, repo.name, "main", { limit: 1 });
0b1c50f60 const latest = data.commits[0];
0b1c50f61 if (latest) return { id: repo.id, timestamp: latest.timestamp };
0b1c50f62 } catch {}
0b1c50f63 return null;
0b1c50f64 })
0b1c50f65 ).then((results) => {
0b1c50f66 const times: Record<number, number> = {};
0b1c50f67 for (const r of results) {
0b1c50f68 if (r) times[r.id] = r.timestamp;
0b1c50f69 }
0b1c50f70 setLastCommitTimes(times);
0b1c50f71 });
0b1c50f72 }, [repoList]);
0b1c50f73
60644e774 return (
60644e775 <div className="max-w-3xl mx-auto px-4 py-6">
79efd4176 <h1 className="text-lg mb-1">
79efd4177 {isOrg && orgData
79efd4178 ? orgData.org.display_name || orgData.org.name
79efd4179 : owner}
79efd4180 </h1>
79efd4181 {isOrg && orgData && (
79efd4182 <p className="text-xs mb-6" style={{ color: "var(--text-faint)" }}>
79efd4183 {orgData.members.length} member
79efd4184 {orgData.members.length !== 1 ? "s" : ""}
79efd4185 {" \u00b7 "}
79efd4186 {orgData.members.map((m) => m.username).join(", ")}
79efd4187 </p>
79efd4188 )}
79efd4189
79efd4190 {!isOrg && <div className="mb-6" />}
60644e791
13a9fd192 {user && (
13a9fd193 <div className="mb-4">
13a9fd194 <Link
13a9fd195 href={`/new?owner=${encodeURIComponent(owner)}`}
13a9fd196 className="inline-block px-3 py-1.5 text-sm"
13a9fd197 style={{
13a9fd198 backgroundColor: "var(--accent)",
13a9fd199 color: "var(--accent-text)",
13a9fd1100 }}
13a9fd1101 >
13a9fd1102 New repository
13a9fd1103 </Link>
13a9fd1104 </div>
13a9fd1105 )}
13a9fd1106
60644e7107 {!loaded ? (
60644e7108 <ListSkeleton rows={3} />
60644e7109 ) : repoList.length === 0 ? (
60644e7110 <p className="text-sm" style={{ color: "var(--text-muted)" }}>
79efd41111 No repositories found for this {isOrg ? "organization" : "user"}.
60644e7112 </p>
60644e7113 ) : (
60644e7114 <div
60644e7115 style={{
60644e7116 border: "1px solid var(--border-subtle)",
60644e7117 }}
60644e7118 >
60644e7119 {repoList.map((repo, i) => (
60644e7120 <Link
60644e7121 key={repo.id}
60644e7122 href={`/${repo.owner_name}/${repo.name}`}
60644e7123 className="flex items-baseline justify-between py-2.5 px-3 text-sm hover-row"
60644e7124 style={{
60644e7125 borderTop: i > 0 ? "1px solid var(--divide)" : undefined,
60644e7126 }}
60644e7127 >
8a2c7d4128 <div className="min-w-0 truncate">
60644e7129 <span style={{ color: "var(--accent)" }}>{repo.name}</span>
60644e7130 {repo.description && (
60644e7131 <span
8a2c7d4132 className="ml-3 text-xs hidden sm:inline"
60644e7133 style={{ color: "var(--text-faint)" }}
60644e7134 >
60644e7135 {repo.description}
60644e7136 </span>
60644e7137 )}
60644e7138 </div>
0b1c50f139 {lastCommitTimes[repo.id] && (
0b1c50f140 <span
0b1c50f141 className="text-xs shrink-0 ml-4"
0b1c50f142 style={{ color: "var(--text-faint)" }}
0b1c50f143 >
0b1c50f144 {timeAgo(lastCommitTimes[repo.id])}
0b1c50f145 </span>
0b1c50f146 )}
60644e7147 </Link>
60644e7148 ))}
60644e7149 </div>
60644e7150 )}
60644e7151 </div>
60644e7152 );
60644e7153}