mirror of
https://gitbruv.vercel.app/api/git/bruv/gitbruv.git
synced 2025-12-20 23:24:09 +01:00
add tabs
This commit is contained in:
parent
c768b15bd7
commit
1a8b4c881f
6 changed files with 247 additions and 69 deletions
|
|
@ -721,3 +721,53 @@ export async function getRepoCommitCountCached(owner: string, repoName: string)
|
||||||
|
|
||||||
return getCachedCommitCount(owner, repoName, user.id, repo.defaultBranch);
|
return getCachedCommitCount(owner, repoName, user.id, repo.defaultBranch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUserStarredRepos(username: string) {
|
||||||
|
const user = await db.query.users.findFirst({
|
||||||
|
where: eq(users.username, username),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const starredRepos = await db
|
||||||
|
.select({
|
||||||
|
id: repositories.id,
|
||||||
|
name: repositories.name,
|
||||||
|
description: repositories.description,
|
||||||
|
visibility: repositories.visibility,
|
||||||
|
defaultBranch: repositories.defaultBranch,
|
||||||
|
createdAt: repositories.createdAt,
|
||||||
|
updatedAt: repositories.updatedAt,
|
||||||
|
ownerId: repositories.ownerId,
|
||||||
|
ownerUsername: users.username,
|
||||||
|
ownerName: users.name,
|
||||||
|
ownerImage: users.image,
|
||||||
|
starredAt: stars.createdAt,
|
||||||
|
starCount: sql<number>`(SELECT COUNT(*) FROM stars WHERE stars.repository_id = ${repositories.id})`.as("star_count"),
|
||||||
|
})
|
||||||
|
.from(stars)
|
||||||
|
.innerJoin(repositories, eq(stars.repositoryId, repositories.id))
|
||||||
|
.innerJoin(users, eq(repositories.ownerId, users.id))
|
||||||
|
.where(and(eq(stars.userId, user.id), eq(repositories.visibility, "public")))
|
||||||
|
.orderBy(desc(stars.createdAt));
|
||||||
|
|
||||||
|
return starredRepos.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
name: r.name,
|
||||||
|
description: r.description,
|
||||||
|
visibility: r.visibility as "public" | "private",
|
||||||
|
defaultBranch: r.defaultBranch,
|
||||||
|
createdAt: r.createdAt,
|
||||||
|
updatedAt: r.updatedAt,
|
||||||
|
starCount: Number(r.starCount),
|
||||||
|
starredAt: r.starredAt,
|
||||||
|
owner: {
|
||||||
|
id: r.ownerId,
|
||||||
|
username: r.ownerUsername,
|
||||||
|
name: r.ownerName,
|
||||||
|
image: r.ownerImage,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,16 @@ import { notFound } from "next/navigation";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import { users } from "@/db/schema";
|
import { users } from "@/db/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { getUserRepositoriesWithStars } from "@/actions/repositories";
|
import { getUserRepositoriesWithStars, getUserStarredRepos } from "@/actions/repositories";
|
||||||
import { RepoList } from "@/components/repo-list";
|
import { RepoList } from "@/components/repo-list";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { CalendarDays, GitBranch, MapPin, Link as LinkIcon, Loader2 } from "lucide-react";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { CalendarDays, GitBranch, MapPin, Link as LinkIcon, Loader2, Star, BookOpen } from "lucide-react";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { GithubIcon, XIcon, LinkedInIcon } from "@/components/icons";
|
import { GithubIcon, XIcon, LinkedInIcon } from "@/components/icons";
|
||||||
|
|
||||||
async function RepoSection({ username }: { username: string }) {
|
async function RepositoriesTab({ username }: { username: string }) {
|
||||||
const repos = await getUserRepositoriesWithStars(username);
|
const repos = await getUserRepositoriesWithStars(username);
|
||||||
|
|
||||||
if (repos.length === 0) {
|
if (repos.length === 0) {
|
||||||
|
|
@ -27,7 +28,23 @@ async function RepoSection({ username }: { username: string }) {
|
||||||
return <RepoList repos={repos} username={username} />;
|
return <RepoList repos={repos} username={username} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function RepoSkeleton() {
|
async function StarredTab({ username }: { username: string }) {
|
||||||
|
const repos = await getUserStarredRepos(username);
|
||||||
|
|
||||||
|
if (repos.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="border border-dashed border-border rounded-lg p-12 text-center">
|
||||||
|
<Star className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
|
||||||
|
<h3 className="text-lg font-medium mb-2">No starred repositories</h3>
|
||||||
|
<p className="text-muted-foreground">This user hasn't starred any repositories yet.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <RepoList repos={repos} username={username} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{[...Array(3)].map((_, i) => (
|
{[...Array(3)].map((_, i) => (
|
||||||
|
|
@ -44,8 +61,15 @@ function RepoSkeleton() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function ProfilePage({ params }: { params: Promise<{ username: string }> }) {
|
export default async function ProfilePage({
|
||||||
|
params,
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ username: string }>;
|
||||||
|
searchParams: Promise<{ tab?: string }>;
|
||||||
|
}) {
|
||||||
const { username } = await params;
|
const { username } = await params;
|
||||||
|
const { tab } = await searchParams;
|
||||||
|
|
||||||
const user = await db.query.users.findFirst({
|
const user = await db.query.users.findFirst({
|
||||||
where: eq(users.username, username),
|
where: eq(users.username, username),
|
||||||
|
|
@ -55,6 +79,8 @@ export default async function ProfilePage({ params }: { params: Promise<{ userna
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const activeTab = tab === "starred" ? "starred" : "repositories";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container px-4 py-8">
|
<div className="container px-4 py-8">
|
||||||
<div className="flex flex-col lg:flex-row gap-8">
|
<div className="flex flex-col lg:flex-row gap-8">
|
||||||
|
|
@ -122,14 +148,42 @@ export default async function ProfilePage({ params }: { params: Promise<{ userna
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 mb-6">
|
<Tabs defaultValue={activeTab} className="w-full">
|
||||||
<GitBranch className="h-5 w-5" />
|
<TabsList className="mb-6 w-full justify-start bg-transparent border-b border-border rounded-none p-0 h-auto">
|
||||||
<h2 className="text-xl font-semibold">Repositories</h2>
|
<TabsTrigger
|
||||||
</div>
|
value="repositories"
|
||||||
|
className="rounded-none border-b-2 border-transparent data-[state=active]:border-accent data-[state=active]:bg-transparent px-4 py-3 gap-2"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Link href={`/${username}`}>
|
||||||
|
<BookOpen className="h-4 w-4" />
|
||||||
|
Repositories
|
||||||
|
</Link>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="starred"
|
||||||
|
className="rounded-none border-b-2 border-transparent data-[state=active]:border-accent data-[state=active]:bg-transparent px-4 py-3 gap-2"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Link href={`/${username}?tab=starred`}>
|
||||||
|
<Star className="h-4 w-4" />
|
||||||
|
Starred
|
||||||
|
</Link>
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
<Suspense fallback={<RepoSkeleton />}>
|
<TabsContent value="repositories" className="mt-0">
|
||||||
<RepoSection username={username} />
|
<Suspense fallback={<TabSkeleton />}>
|
||||||
</Suspense>
|
<RepositoriesTab username={username} />
|
||||||
|
</Suspense>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="starred" className="mt-0">
|
||||||
|
<Suspense fallback={<TabSkeleton />}>
|
||||||
|
<StarredTab username={username} />
|
||||||
|
</Suspense>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
9
bun.lock
9
bun.lock
|
|
@ -14,6 +14,7 @@
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tanstack/react-query": "^5.90.12",
|
"@tanstack/react-query": "^5.90.12",
|
||||||
"@vercel/analytics": "^1.6.1",
|
"@vercel/analytics": "^1.6.1",
|
||||||
|
|
@ -419,6 +420,8 @@
|
||||||
|
|
||||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
|
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
|
||||||
|
|
||||||
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
|
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
|
||||||
|
|
||||||
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||||
|
|
@ -1635,6 +1638,10 @@
|
||||||
|
|
||||||
"@radix-ui/react-roving-focus/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
"@radix-ui/react-roving-focus/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-tabs/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-tabs/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
|
||||||
|
|
@ -1739,6 +1746,8 @@
|
||||||
|
|
||||||
"@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||||
|
|
||||||
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||||
|
|
|
||||||
|
|
@ -11,69 +11,67 @@ type Repository = {
|
||||||
visibility: "public" | "private";
|
visibility: "public" | "private";
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
starCount?: number;
|
starCount?: number;
|
||||||
|
owner?: {
|
||||||
|
username: string;
|
||||||
|
name: string | null;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function RepoList({
|
export function RepoList({ repos, username }: { repos: Repository[]; username?: string }) {
|
||||||
repos,
|
|
||||||
username,
|
|
||||||
}: {
|
|
||||||
repos: Repository[];
|
|
||||||
username: string;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{repos.map((repo) => (
|
{repos.map((repo) => {
|
||||||
<Link
|
const ownerUsername = repo.owner?.username || username || "";
|
||||||
key={repo.id}
|
const showOwner = repo.owner && repo.owner.username !== username;
|
||||||
href={`/${username}/${repo.name}`}
|
|
||||||
className="block p-5 rounded-xl border border-border bg-card hover:border-accent/50 transition-all duration-200 group"
|
return (
|
||||||
>
|
<Link
|
||||||
<div className="flex items-start justify-between gap-4">
|
key={repo.id}
|
||||||
<div className="min-w-0 flex-1">
|
href={`/${ownerUsername}/${repo.name}`}
|
||||||
<div className="flex items-center gap-3 mb-1.5">
|
className="block p-5 rounded-xl border border-border bg-card hover:border-accent/50 transition-all duration-200 group"
|
||||||
<span className="font-semibold text-accent group-hover:underline text-lg">
|
>
|
||||||
{repo.name}
|
<div className="flex items-start justify-between gap-4">
|
||||||
</span>
|
<div className="min-w-0 flex-1">
|
||||||
<span
|
<div className="flex items-center gap-3 mb-1.5">
|
||||||
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium border ${
|
<span className="font-semibold text-accent group-hover:underline text-lg">
|
||||||
repo.visibility === "private"
|
{showOwner && <span className="text-muted-foreground font-normal">{repo.owner?.username}/</span>}
|
||||||
? "border-yellow-500/30 text-yellow-500 bg-yellow-500/10"
|
{repo.name}
|
||||||
: "border-border text-muted-foreground bg-secondary"
|
</span>
|
||||||
}`}
|
<span
|
||||||
>
|
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium border ${
|
||||||
{repo.visibility === "private" ? (
|
repo.visibility === "private"
|
||||||
<>
|
? "border-yellow-500/30 text-yellow-500 bg-yellow-500/10"
|
||||||
<Lock className="h-3 w-3" />
|
: "border-border text-muted-foreground bg-secondary"
|
||||||
Private
|
}`}
|
||||||
</>
|
>
|
||||||
) : (
|
{repo.visibility === "private" ? (
|
||||||
<>
|
<>
|
||||||
<Globe className="h-3 w-3" />
|
<Lock className="h-3 w-3" />
|
||||||
Public
|
Private
|
||||||
</>
|
</>
|
||||||
)}
|
) : (
|
||||||
</span>
|
<>
|
||||||
</div>
|
<Globe className="h-3 w-3" />
|
||||||
{repo.description && (
|
Public
|
||||||
<p className="text-sm text-muted-foreground line-clamp-2 mt-2">
|
</>
|
||||||
{repo.description}
|
)}
|
||||||
</p>
|
</span>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-end gap-1 shrink-0 pt-1">
|
|
||||||
{typeof repo.starCount === "number" && repo.starCount > 0 && (
|
|
||||||
<div className="flex items-center gap-1 text-muted-foreground">
|
|
||||||
<Star className="h-3.5 w-3.5" />
|
|
||||||
<span className="text-xs">{repo.starCount}</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{repo.description && <p className="text-sm text-muted-foreground line-clamp-2 mt-2">{repo.description}</p>}
|
||||||
<p className="text-xs text-muted-foreground">
|
</div>
|
||||||
{formatDistanceToNow(new Date(repo.updatedAt), { addSuffix: true })}
|
<div className="flex flex-col items-end gap-1 shrink-0 pt-1">
|
||||||
</p>
|
{typeof repo.starCount === "number" && repo.starCount > 0 && (
|
||||||
|
<div className="flex items-center gap-1 text-muted-foreground">
|
||||||
|
<Star className="h-3.5 w-3.5" />
|
||||||
|
<span className="text-xs">{repo.starCount}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-muted-foreground">{formatDistanceToNow(new Date(repo.updatedAt), { addSuffix: true })}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
</Link>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
66
components/ui/tabs.tsx
Normal file
66
components/ui/tabs.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Tabs({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Root
|
||||||
|
data-slot="tabs"
|
||||||
|
className={cn("flex flex-col gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
data-slot="tabs-list"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsTrigger({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
data-slot="tabs-trigger"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
data-slot="tabs-content"
|
||||||
|
className={cn("flex-1 outline-none", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tanstack/react-query": "^5.90.12",
|
"@tanstack/react-query": "^5.90.12",
|
||||||
"@vercel/analytics": "^1.6.1",
|
"@vercel/analytics": "^1.6.1",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue