web/app/collab/collab-repo-list.tsxblame
View source
0b4b5821"use client";
0b4b5822
0b4b5823import { useState } from "react";
0b4b5824import Link from "next/link";
0b4b5825import { CollabLogo } from "@/app/components/collab-logo";
0b4b5826
0b4b5827interface Repo {
0b4b5828 name: string;
0b4b5829 owner_name: string;
0b4b58210 description: string | null;
0b4b58211 last_commit_ts: number | null;
0b4b58212 updated_at: string | null;
0b4b58213}
0b4b58214
0b4b58215function timeAgo(ts: number): string {
0b4b58216 const secs = Math.floor(Date.now() / 1000) - ts;
0b4b58217 if (secs < 60) return "just now";
0b4b58218 if (secs < 3600) return Math.floor(secs / 60) + "m ago";
0b4b58219 if (secs < 86400) return Math.floor(secs / 3600) + "h ago";
0b4b58220 if (secs < 2592000) return Math.floor(secs / 86400) + "d ago";
0b4b58221 return Math.floor(secs / 2592000) + "mo ago";
0b4b58222}
0b4b58223
0b4b58224function groupByOwner(repos: Repo[]): { owner: string; repos: Repo[] }[] {
0b4b58225 const map = new Map<string, Repo[]>();
0b4b58226 for (const r of repos) {
0b4b58227 const list = map.get(r.owner_name) ?? [];
0b4b58228 list.push(r);
0b4b58229 map.set(r.owner_name, list);
0b4b58230 }
0b4b58231 return Array.from(map, ([owner, repos]) => ({ owner, repos }));
0b4b58232}
0b4b58233
0b4b58234export function CollabRepoList({ repos }: { repos: Repo[] }) {
0b4b58235 const [query, setQuery] = useState("");
0b4b58236
0b4b58237 const sorted = [...repos].sort((a, b) => {
0b4b58238 const aTs = a.last_commit_ts ?? (a.updated_at ? Math.floor(new Date(a.updated_at).getTime() / 1000) : 0);
0b4b58239 const bTs = b.last_commit_ts ?? (b.updated_at ? Math.floor(new Date(b.updated_at).getTime() / 1000) : 0);
0b4b58240 return bTs - aTs;
0b4b58241 });
0b4b58242
0b4b58243 const filtered = query
0b4b58244 ? sorted.filter((r) =>
0b4b58245 `${r.owner_name} ${r.name} ${r.description ?? ""}`.toLowerCase().includes(query.toLowerCase())
0b4b58246 )
0b4b58247 : sorted;
0b4b58248
0b4b58249 const groups = groupByOwner(filtered);
0b4b58250
0b4b58251 return (
0b4b58252 <div className="max-w-3xl mx-auto px-4 py-6 flex flex-col gap-4">
0b4b58253 <input
0b4b58254 type="text"
0b4b58255 placeholder="Search repositories"
0b4b58256 value={query}
0b4b58257 onChange={(e) => setQuery(e.target.value)}
0b4b58258 className="w-full px-3 py-2 text-sm"
0b4b58259 style={{
0b4b58260 backgroundColor: "var(--bg-input)",
0b4b58261 border: "1px solid var(--border)",
0b4b58262 color: "var(--text-primary)",
0b4b58263 outline: "none",
0b4b58264 }}
0b4b58265 />
0b4b58266
0b4b58267 {groups.length === 0 && (
0b4b58268 <div className="py-8 text-center">
0b4b58269 <div className="mx-auto mb-4 w-fit opacity-50">
0b4b58270 <CollabLogo size={48} />
0b4b58271 </div>
0b4b58272 <p className="text-sm" style={{ color: "var(--text-faint)" }}>
0b4b58273 No repositories found.
0b4b58274 </p>
0b4b58275 </div>
0b4b58276 )}
0b4b58277
0b4b58278 {groups.map((group) => (
0b4b58279 <div key={group.owner}>
0b4b58280 <div
0b4b58281 className="text-xs font-medium uppercase tracking-wide mb-2 px-1"
0b4b58282 style={{ color: "var(--text-faint)" }}
0b4b58283 >
0b4b58284 {group.owner}
0b4b58285 </div>
0b4b58286 <div>
0b4b58287 {group.repos.map((r) => {
0b4b58288 const commitTs = r.last_commit_ts ?? null;
0b4b58289 const updatedTs = r.updated_at
0b4b58290 ? Math.floor(new Date(r.updated_at).getTime() / 1000)
0b4b58291 : null;
0b4b58292 const ts = commitTs ?? updatedTs;
0b4b58293 const label = commitTs ? "Pushed" : updatedTs ? "Updated" : null;
0b4b58294
0b4b58295 return (
0b4b58296 <Link
0b4b58297 key={`${r.owner_name}/${r.name}`}
0b4b58298 href={`/${r.owner_name}/${r.name}`}
0b4b58299 className="flex items-center justify-between py-2.5 px-1 hover-row"
0b4b582100 style={{
0b4b582101 borderBottom: "1px solid var(--divide)",
0b4b582102 textDecoration: "none",
0b4b582103 color: "inherit",
0b4b582104 }}
0b4b582105 >
0b4b582106 <div style={{ minWidth: 0 }}>
0b4b582107 <div className="text-sm" style={{ color: "var(--accent)" }}>
0b4b582108 {r.name}
0b4b582109 </div>
0b4b582110 <div className="text-xs" style={{ color: "var(--text-faint)" }}>
0b4b582111 {r.description || "No description"}
0b4b582112 </div>
0b4b582113 </div>
0b4b582114 {ts && label && (
0b4b582115 <span
0b4b582116 className="text-xs shrink-0 ml-4"
0b4b582117 style={{ color: "var(--text-faint)" }}
0b4b582118 >
0b4b582119 {label} {timeAgo(ts)}
0b4b582120 </span>
0b4b582121 )}
0b4b582122 </Link>
0b4b582123 );
0b4b582124 })}
0b4b582125 </div>
0b4b582126 </div>
0b4b582127 ))}
0b4b582128 </div>
0b4b582129 );
0b4b582130}