1
0
Fork 0
mirror of https://gitbruv.vercel.app/api/git/bruv/gitbruv.git synced 2025-12-20 23:24:09 +01:00
This commit is contained in:
Ahmet Kilinc 2025-12-20 11:53:09 +00:00
parent c768b15bd7
commit 1a8b4c881f
6 changed files with 247 additions and 69 deletions

View file

@ -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,
},
}));
}

View file

@ -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&apos;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>

View file

@ -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=="],

View file

@ -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
View 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 }

View file

@ -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",