TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import Link from "next/link";2import { ArrowRight, FolderKanban } from "lucide-react";3import { getWorkspace } from "@/lib/session";4import { projectStatsByProject } from "@/lib/queries/account";5import { formatDateOnly, formatNumber, formatUsd, truncate } from "@/lib/format";6import { PageHeader } from "@/components/ui/page-header";7import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";8import { Badge } from "@/components/ui/badge";9import { Button } from "@/components/ui/button";10import { EmptyState } from "@/components/ui/empty-state";11import { CreateProjectDialog } from "@/components/dashboard/projects/create-project-dialog";12import { SelectProjectButton } from "@/components/dashboard/projects/project-actions";1314export const dynamic = "force-dynamic";1516export const metadata = { title: "Projects · Fetcha" };1718export default async function ProjectsPage({ searchParams }: { searchParams: Promise<{ new?: string }> }) {19 const sp = await searchParams;20 const ws = await getWorkspace();21 const stats = await projectStatsByProject(ws.organization.id);2223 return (24 <div className="grid gap-6">25 <PageHeader26 title="Projects"27 description="Each project has its own API keys, request log, defaults and spending limits. Switch the current project from the top bar or here."28 actions={<CreateProjectDialog defaultOpen={sp.new === "1"} />}29 />3031 {ws.projects.length === 0 ? (32 <EmptyState icon={FolderKanban} title="No projects" description="Create a project to start generating API keys." action={<CreateProjectDialog />} />33 ) : (34 <div className="overflow-hidden rounded-lg border border-border bg-bg-elevated shadow-xs">35 <Table>36 <TableHeader>37 <TableRow className="hover:bg-transparent">38 <TableHead>Project</TableHead>39 <TableHead>Environment</TableHead>40 <TableHead className="text-right">Requests · month</TableHead>41 <TableHead className="text-right">Spend · month</TableHead>42 <TableHead className="text-right">Keys</TableHead>43 <TableHead>Created</TableHead>44 <TableHead className="text-right">45 <span className="sr-only">Actions</span>46 </TableHead>47 </TableRow>48 </TableHeader>49 <TableBody>50 {ws.projects.map((p) => {51 const s = stats.get(p.id) ?? { requests: 0, spendUsd: 0, activeKeys: 0 };52 const current = p.id === ws.project.id;53 return (54 <TableRow key={p.id}>55 <TableCell>56 <div className="flex min-w-0 flex-col gap-0.5">57 <div className="flex items-center gap-2">58 <Link href={`/dashboard/projects/${p.id}`} className="font-medium text-fg underline-offset-4 hover:underline">59 {p.name}60 </Link>61 {current ? (62 <Badge variant="accent" dot>63 Current64 </Badge>65 ) : null}66 </div>67 {p.description ? <span className="text-[12.5px] text-fg-muted">{truncate(p.description, 80)}</span> : <span className="font-mono text-[11.5px] text-fg-subtle">{p.id}</span>}68 </div>69 </TableCell>70 <TableCell>71 <Badge variant={p.environment === "production" ? "success" : "warning"}>{p.environment}</Badge>72 </TableCell>73 <TableCell className="text-right font-mono tabular">{formatNumber(s.requests)}</TableCell>74 <TableCell className="text-right font-mono tabular">{formatUsd(s.spendUsd)}</TableCell>75 <TableCell className="text-right font-mono tabular">76 <Link href={`/dashboard/api-keys?project=${p.id}`} className="underline-offset-4 hover:underline">77 {s.activeKeys}78 </Link>79 </TableCell>80 <TableCell className="whitespace-nowrap tabular text-fg-muted">{formatDateOnly(p.createdAt)}</TableCell>81 <TableCell className="text-right">82 <div className="flex items-center justify-end gap-1.5">83 {!current ? <SelectProjectButton projectId={p.id} /> : null}84 <Button asChild size="xs" variant="ghost">85 <Link href={`/dashboard/projects/${p.id}`}>86 Open <ArrowRight />87 </Link>88 </Button>89 </div>90 </TableCell>91 </TableRow>92 );93 })}94 </TableBody>95 </Table>96 </div>97 )}98 </div>99 );100}101