From 1a8b4c881f1526bd8e77b37913397adb9a2cf720 Mon Sep 17 00:00:00 2001 From: Ahmet Kilinc Date: Sat, 20 Dec 2025 11:53:09 +0000 Subject: [PATCH] add tabs --- actions/repositories.ts | 50 +++++++++++++++ app/(main)/[username]/page.tsx | 78 +++++++++++++++++++---- bun.lock | 9 +++ components/repo-list.tsx | 112 ++++++++++++++++----------------- components/ui/tabs.tsx | 66 +++++++++++++++++++ package.json | 1 + 6 files changed, 247 insertions(+), 69 deletions(-) create mode 100644 components/ui/tabs.tsx diff --git a/actions/repositories.ts b/actions/repositories.ts index 8d5ae92..bc11bb9 100644 --- a/actions/repositories.ts +++ b/actions/repositories.ts @@ -721,3 +721,53 @@ export async function getRepoCommitCountCached(owner: string, repoName: string) 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`(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, + }, + })); +} diff --git a/app/(main)/[username]/page.tsx b/app/(main)/[username]/page.tsx index d7004a1..90371c8 100644 --- a/app/(main)/[username]/page.tsx +++ b/app/(main)/[username]/page.tsx @@ -3,15 +3,16 @@ import { notFound } from "next/navigation"; import { db } from "@/db"; import { users } from "@/db/schema"; import { eq } from "drizzle-orm"; -import { getUserRepositoriesWithStars } from "@/actions/repositories"; +import { getUserRepositoriesWithStars, getUserStarredRepos } from "@/actions/repositories"; import { RepoList } from "@/components/repo-list"; 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 Link from "next/link"; import { GithubIcon, XIcon, LinkedInIcon } from "@/components/icons"; -async function RepoSection({ username }: { username: string }) { +async function RepositoriesTab({ username }: { username: string }) { const repos = await getUserRepositoriesWithStars(username); if (repos.length === 0) { @@ -27,7 +28,23 @@ async function RepoSection({ username }: { username: string }) { return ; } -function RepoSkeleton() { +async function StarredTab({ username }: { username: string }) { + const repos = await getUserStarredRepos(username); + + if (repos.length === 0) { + return ( +
+ +

No starred repositories

+

This user hasn't starred any repositories yet.

+
+ ); + } + + return ; +} + +function TabSkeleton() { return (
{[...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 { tab } = await searchParams; const user = await db.query.users.findFirst({ where: eq(users.username, username), @@ -55,6 +79,8 @@ export default async function ProfilePage({ params }: { params: Promise<{ userna notFound(); } + const activeTab = tab === "starred" ? "starred" : "repositories"; + return (
@@ -122,14 +148,42 @@ export default async function ProfilePage({ params }: { params: Promise<{ userna
-
- -

Repositories

-
+ + + + + + Repositories + + + + + + Starred + + + - }> - - + + }> + + + + + + }> + + + +
diff --git a/bun.lock b/bun.lock index d34288a..be076c2 100644 --- a/bun.lock +++ b/bun.lock @@ -14,6 +14,7 @@ "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", "@tailwindcss/typography": "^0.5.19", "@tanstack/react-query": "^5.90.12", "@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-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-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-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/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-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=="], "@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=="], diff --git a/components/repo-list.tsx b/components/repo-list.tsx index ad8542f..fb5de66 100644 --- a/components/repo-list.tsx +++ b/components/repo-list.tsx @@ -11,69 +11,67 @@ type Repository = { visibility: "public" | "private"; updatedAt: Date; starCount?: number; + owner?: { + username: string; + name: string | null; + }; }; -export function RepoList({ - repos, - username, -}: { - repos: Repository[]; - username: string; -}) { +export function RepoList({ repos, username }: { repos: Repository[]; username?: string }) { return (
- {repos.map((repo) => ( - -
-
-
- - {repo.name} - - - {repo.visibility === "private" ? ( - <> - - Private - - ) : ( - <> - - Public - - )} - -
- {repo.description && ( -

- {repo.description} -

- )} -
-
- {typeof repo.starCount === "number" && repo.starCount > 0 && ( -
- - {repo.starCount} + {repos.map((repo) => { + const ownerUsername = repo.owner?.username || username || ""; + const showOwner = repo.owner && repo.owner.username !== username; + + return ( + +
+
+
+ + {showOwner && {repo.owner?.username}/} + {repo.name} + + + {repo.visibility === "private" ? ( + <> + + Private + + ) : ( + <> + + Public + + )} +
- )} -

- {formatDistanceToNow(new Date(repo.updatedAt), { addSuffix: true })} -

+ {repo.description &&

{repo.description}

} +
+
+ {typeof repo.starCount === "number" && repo.starCount > 0 && ( +
+ + {repo.starCount} +
+ )} +

{formatDistanceToNow(new Date(repo.updatedAt), { addSuffix: true })}

+
-
- - ))} + + ); + })}
); } diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx new file mode 100644 index 0000000..497ba5e --- /dev/null +++ b/components/ui/tabs.tsx @@ -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) { + return ( + + ) +} + +function TabsList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsTrigger({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/package.json b/package.json index 17f687a..af8027f 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", "@tailwindcss/typography": "^0.5.19", "@tanstack/react-query": "^5.90.12", "@vercel/analytics": "^1.6.1",