190 lines
6.8 KiB
TypeScript
190 lines
6.8 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import {
|
|
GitFork,
|
|
Loader2,
|
|
Star,
|
|
Clock,
|
|
Lock,
|
|
Globe,
|
|
} from "lucide-react";
|
|
import { api } from "@/client";
|
|
import RepoDrawer from "./repo-drawer";
|
|
import type { RepoLinkMatch } from "./repo-link-parser";
|
|
|
|
type RepoEmbedData = {
|
|
name: string;
|
|
description: string | null;
|
|
default_branch: string;
|
|
visibility: string;
|
|
updated_at: string;
|
|
language: string | null;
|
|
star_count: number;
|
|
fork_count: number;
|
|
topics: string[];
|
|
};
|
|
|
|
function languageColor(lang: string): string {
|
|
const map: Record<string, string> = {
|
|
Rust: "#DEA584", TypeScript: "#3178C6", JavaScript: "#F7DF1E",
|
|
Python: "#3572A5", Go: "#00ADD8", Java: "#B07219",
|
|
Kotlin: "#A97BFF", Swift: "#F05138", C: "#555555",
|
|
"C++": "#F34B7D", "C#": "#178600", Ruby: "#701516",
|
|
Zig: "#EC915C", Elixir: "#6E4A7E", Haskell: "#5E5086",
|
|
CSS: "#563D7C", HTML: "#E34C26", Shell: "#89E051",
|
|
PHP: "#4F5D95", Dart: "#00B4AB", Scala: "#C22D40",
|
|
R: "#198CE7", Lua: "#000080", Vue: "#41B883",
|
|
Svelte: "#FF3E00", MDX: "#FCB32C", Dockerfile: "#384D54",
|
|
Makefile: "#427819", Markdown: "#083FA1", Nix: "#7E7EFF",
|
|
OCaml: "#3BE133", "Objective-C": "#438EFF", Perl: "#0298C3",
|
|
Erlang: "#B83998", CMake: "#DA3434", PowerShell: "#012456",
|
|
SQL: "#E38C00", Solidity: "#AA6746", Terraform: "#7B42BC",
|
|
};
|
|
return map[lang] ?? "#6B7280";
|
|
}
|
|
|
|
function formatCount(n: number): string {
|
|
if (n === 0) return "";
|
|
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
return n.toString();
|
|
}
|
|
|
|
function timeAgo(iso: string): string {
|
|
const diff = Date.now() - new Date(iso).getTime();
|
|
const days = Math.floor(diff / 86400000);
|
|
if (days < 1) return "today";
|
|
if (days === 1) return "yesterday";
|
|
if (days < 30) return `${days}d ago`;
|
|
const months = Math.floor(days / 30);
|
|
if (months < 12) return `${months}mo ago`;
|
|
return `${Math.floor(months / 12)}y ago`;
|
|
}
|
|
|
|
export default function RepoEmbedCard({ link }: { link: RepoLinkMatch }) {
|
|
const [data, setData] = useState<RepoEmbedData | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState(false);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
setLoading(true);
|
|
setError(false);
|
|
|
|
api
|
|
.get<RepoEmbedData>(
|
|
`/api/v1/workspace/${link.workspace}/repos/${link.repo}/embed-card`,
|
|
)
|
|
.then((res) => {
|
|
if (!cancelled) setData(res.data);
|
|
})
|
|
.catch(() => {
|
|
if (!cancelled) setError(true);
|
|
})
|
|
.finally(() => {
|
|
if (!cancelled) setLoading(false);
|
|
});
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [link.workspace, link.repo]);
|
|
|
|
return (
|
|
<RepoDrawer repo={link.repo} workspace={link.workspace}>
|
|
<div
|
|
className="mt-2 block max-w-[420px] cursor-pointer rounded-xl border border-border/30 bg-muted/[0.03] p-4 transition-[background-color,border-color,color,opacity,box-shadow,transform] duration-200 hover:border-primary/20 hover:bg-muted/[0.08] hover:shadow-sm"
|
|
>
|
|
{loading ? (
|
|
<div className="flex items-center gap-2 py-2 text-[13px] text-muted-foreground/50">
|
|
<Loader2 className="size-4 animate-spin" />
|
|
Loading repo info…
|
|
</div>
|
|
) : error || !data ? (
|
|
<div className="flex items-center gap-2 py-2">
|
|
<div className="grid size-8 shrink-0 place-items-center rounded-lg bg-muted/40">
|
|
<div className="grid size-7 place-items-center rounded-md bg-gradient-to-br from-primary/30 to-primary/10 text-[10px] font-bold text-primary/70">
|
|
{link.repo.charAt(0).toUpperCase()}
|
|
</div>
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-[13px] font-semibold text-foreground/70">
|
|
{link.workspace}/
|
|
<span className="text-primary/80">{link.repo}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="flex items-start gap-3">
|
|
<div className="grid size-8 shrink-0 place-items-center rounded-lg bg-muted/40">
|
|
<div className="grid size-7 place-items-center rounded-md bg-gradient-to-br from-primary/30 to-primary/10 text-[10px] font-bold text-primary/70">
|
|
{data.name.charAt(0).toUpperCase()}
|
|
</div>
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-[13px] font-semibold text-foreground/90">
|
|
{link.workspace}/
|
|
<span className="text-primary/80">{link.repo}</span>
|
|
</p>
|
|
{data.description && (
|
|
<p className="mt-0.5 line-clamp-2 text-[12px] leading-relaxed text-muted-foreground/60">
|
|
{data.description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-3 flex flex-wrap items-center gap-3 text-[11px] text-muted-foreground/50">
|
|
{data.language && (
|
|
<span className="inline-flex items-center gap-1.5">
|
|
<span
|
|
className="inline-block size-2.5 rounded-full"
|
|
style={{ backgroundColor: languageColor(data.language) }}
|
|
/>
|
|
{data.language}
|
|
</span>
|
|
)}
|
|
{data.star_count > 0 && (
|
|
<span className="inline-flex items-center gap-1">
|
|
<Star className="size-3" />
|
|
{formatCount(data.star_count)}
|
|
</span>
|
|
)}
|
|
{data.fork_count > 0 && (
|
|
<span className="inline-flex items-center gap-1">
|
|
<GitFork className="size-3" />
|
|
{formatCount(data.fork_count)}
|
|
</span>
|
|
)}
|
|
<span className="inline-flex items-center gap-1">
|
|
{data.visibility === "private" ? (
|
|
<Lock className="size-3" />
|
|
) : (
|
|
<Globe className="size-3" />
|
|
)}
|
|
{data.visibility === "private" ? "private" : "public"}
|
|
</span>
|
|
<span className="inline-flex items-center gap-1">
|
|
<Clock className="size-3" />
|
|
{data.updated_at ? timeAgo(data.updated_at) : ""}
|
|
</span>
|
|
</div>
|
|
|
|
{data.topics.length > 0 && (
|
|
<div className="mt-2.5 flex flex-wrap gap-1.5">
|
|
{data.topics.slice(0, 5).map((t) => (
|
|
<span
|
|
className="inline-block rounded-full bg-primary/[0.06] px-2 py-0.5 text-[10px] font-medium text-primary/70"
|
|
key={t}
|
|
>
|
|
{t}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</RepoDrawer>
|
|
);
|
|
}
|