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