4.2 KB131 lines
Blame
1"use client";
2
3import { useState } from "react";
4import Link from "next/link";
5import { CollabLogo } from "@/app/components/collab-logo";
6
7interface Repo {
8 name: string;
9 owner_name: string;
10 description: string | null;
11 last_commit_ts: number | null;
12 updated_at: string | null;
13}
14
15function timeAgo(ts: number): string {
16 const secs = Math.floor(Date.now() / 1000) - ts;
17 if (secs < 60) return "just now";
18 if (secs < 3600) return Math.floor(secs / 60) + "m ago";
19 if (secs < 86400) return Math.floor(secs / 3600) + "h ago";
20 if (secs < 2592000) return Math.floor(secs / 86400) + "d ago";
21 return Math.floor(secs / 2592000) + "mo ago";
22}
23
24function groupByOwner(repos: Repo[]): { owner: string; repos: Repo[] }[] {
25 const map = new Map<string, Repo[]>();
26 for (const r of repos) {
27 const list = map.get(r.owner_name) ?? [];
28 list.push(r);
29 map.set(r.owner_name, list);
30 }
31 return Array.from(map, ([owner, repos]) => ({ owner, repos }));
32}
33
34export function CollabRepoList({ repos }: { repos: Repo[] }) {
35 const [query, setQuery] = useState("");
36
37 const sorted = [...repos].sort((a, b) => {
38 const aTs = a.last_commit_ts ?? (a.updated_at ? Math.floor(new Date(a.updated_at).getTime() / 1000) : 0);
39 const bTs = b.last_commit_ts ?? (b.updated_at ? Math.floor(new Date(b.updated_at).getTime() / 1000) : 0);
40 return bTs - aTs;
41 });
42
43 const filtered = query
44 ? sorted.filter((r) =>
45 `${r.owner_name} ${r.name} ${r.description ?? ""}`.toLowerCase().includes(query.toLowerCase())
46 )
47 : sorted;
48
49 const groups = groupByOwner(filtered);
50
51 return (
52 <div className="max-w-3xl mx-auto px-4 py-6 flex flex-col gap-4">
53 <input
54 type="text"
55 placeholder="Search repositories"
56 value={query}
57 onChange={(e) => setQuery(e.target.value)}
58 className="w-full px-3 py-2 text-sm"
59 style={{
60 backgroundColor: "var(--bg-input)",
61 border: "1px solid var(--border)",
62 color: "var(--text-primary)",
63 outline: "none",
64 }}
65 />
66
67 {groups.length === 0 && (
68 <div className="py-8 text-center">
69 <div className="mx-auto mb-4 w-fit opacity-50">
70 <CollabLogo size={48} />
71 </div>
72 <p className="text-sm" style={{ color: "var(--text-faint)" }}>
73 No repositories found.
74 </p>
75 </div>
76 )}
77
78 {groups.map((group) => (
79 <div key={group.owner}>
80 <div
81 className="text-xs font-medium uppercase tracking-wide mb-2 px-1"
82 style={{ color: "var(--text-faint)" }}
83 >
84 {group.owner}
85 </div>
86 <div>
87 {group.repos.map((r) => {
88 const commitTs = r.last_commit_ts ?? null;
89 const updatedTs = r.updated_at
90 ? Math.floor(new Date(r.updated_at).getTime() / 1000)
91 : null;
92 const ts = commitTs ?? updatedTs;
93 const label = commitTs ? "Pushed" : updatedTs ? "Updated" : null;
94
95 return (
96 <Link
97 key={`${r.owner_name}/${r.name}`}
98 href={`/${r.owner_name}/${r.name}`}
99 className="flex items-center justify-between py-2.5 px-1 hover-row"
100 style={{
101 borderBottom: "1px solid var(--divide)",
102 textDecoration: "none",
103 color: "inherit",
104 }}
105 >
106 <div style={{ minWidth: 0 }}>
107 <div className="text-sm" style={{ color: "var(--accent)" }}>
108 {r.name}
109 </div>
110 <div className="text-xs" style={{ color: "var(--text-faint)" }}>
111 {r.description || "No description"}
112 </div>
113 </div>
114 {ts && label && (
115 <span
116 className="text-xs shrink-0 ml-4"
117 style={{ color: "var(--text-faint)" }}
118 >
119 {label} {timeAgo(ts)}
120 </span>
121 )}
122 </Link>
123 );
124 })}
125 </div>
126 </div>
127 ))}
128 </div>
129 );
130}
131