feat(navbar): copiar user ID ao lado do nome no menu do usuário

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-04-11 18:16:18 +00:00
parent 5f7bfb98da
commit fa41c78a39
4 changed files with 6 additions and 335 deletions

View File

@@ -5,8 +5,6 @@ import { connection } from "next/server";
import { CompanionTab } from "@/features/settings/components/companion-tab";
import { DeleteAccountForm } from "@/features/settings/components/delete-account-form";
import { DiagnosticsTab } from "@/features/settings/components/diagnostics-tab";
import { fetchDiagnosticsData } from "@/features/settings/diagnostics-queries";
import { PasskeysForm } from "@/features/settings/components/passkeys-form";
import { PreferencesForm } from "@/features/settings/components/preferences-form";
import { UpdateEmailForm } from "@/features/settings/components/update-email-form";
@@ -39,19 +37,6 @@ export default async function Page() {
const { authProvider, userPreferences, userApiTokens } =
await fetchSettingsPageData(session.user.id);
const diagnosticsData = await fetchDiagnosticsData(
session.user.id,
{
id: session.user.id,
name: session.user.name ?? "",
email: session.user.email ?? "",
},
{
createdAt: session.session.createdAt,
expiresAt: session.session.expiresAt,
},
);
return (
<div className="w-full">
<Tabs defaultValue="preferencias" className="w-full">
@@ -65,8 +50,7 @@ export default async function Page() {
<TabsTrigger value="senha">Alterar senha</TabsTrigger>
<TabsTrigger value="passkeys">Passkeys</TabsTrigger>
<TabsTrigger value="email">Alterar e-mail</TabsTrigger>
<TabsTrigger value="diagnostico">Diagnóstico</TabsTrigger>
<TabsTrigger value="deletar" className="text-destructive">
<TabsTrigger value="deletar" className="text-destructive">
Deletar conta
</TabsTrigger>
</TabsList>
@@ -196,23 +180,7 @@ export default async function Page() {
</Card>
</TabsContent>
<TabsContent value="diagnostico" className="mt-4">
<Card className="p-6">
<div className="space-y-4">
<div>
<h2 className="text-xl font-semibold mb-1">Diagnóstico</h2>
<p className="text-sm text-muted-foreground">
Informações técnicas sobre sua conta, sessão e estado do
servidor. Nenhuma credencial ou dado sensível é exibido.
</p>
</div>
<Separator />
<DiagnosticsTab data={diagnosticsData} />
</div>
</Card>
</TabsContent>
<TabsContent value="deletar" className="mt-4">
<TabsContent value="deletar" className="mt-4">
<Card className="p-6">
<div className="space-y-4">
<div>

View File

@@ -1,201 +0,0 @@
"use client";
import {
RiCheckLine,
RiCloseCircleLine,
RiFileCopyLine,
RiWifiLine,
} from "@remixicon/react";
import { useState } from "react";
import { Badge } from "@/shared/components/ui/badge";
import { Separator } from "@/shared/components/ui/separator";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/shared/components/ui/tooltip";
import type { DiagnosticsData } from "../diagnostics-queries";
function CopyButton({ value }: { value: string }) {
const [copied, setCopied] = useState(false);
function handleCopy() {
navigator.clipboard.writeText(value);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
return (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleCopy}
className="shrink-0 text-muted-foreground/50 hover:text-muted-foreground transition-colors"
aria-label="Copiar"
>
{copied ? (
<RiCheckLine className="size-3.5 text-success" />
) : (
<RiFileCopyLine className="size-3.5" />
)}
</button>
</TooltipTrigger>
<TooltipContent>{copied ? "Copiado!" : "Copiar"}</TooltipContent>
</Tooltip>
);
}
function Row({
label,
children,
}: {
label: string;
children: React.ReactNode;
}) {
return (
<div className="flex items-center justify-between gap-4 py-2">
<span className="text-sm text-muted-foreground shrink-0">{label}</span>
<span className="text-sm font-medium text-right flex items-center gap-1.5 min-w-0">
{children}
</span>
</div>
);
}
function Section({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
return (
<div className="space-y-1">
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground/60">
{title}
</p>
<div className="divide-y">{children}</div>
</div>
);
}
function StatusBadge({ ok, labelOk = "Configurado", labelFail = "Não configurado" }: { ok: boolean; labelOk?: string; labelFail?: string }) {
return ok ? (
<Badge variant="outline" className="gap-1 text-success border-success/30 bg-success/5 font-medium">
<RiCheckLine className="size-3" />
{labelOk}
</Badge>
) : (
<Badge variant="outline" className="gap-1 text-muted-foreground font-medium">
<RiCloseCircleLine className="size-3" />
{labelFail}
</Badge>
);
}
function formatDate(date: Date) {
return new Intl.DateTimeFormat("pt-BR", {
dateStyle: "short",
timeStyle: "short",
}).format(new Date(date));
}
export function DiagnosticsTab({ data }: { data: DiagnosticsData }) {
return (
<div className="space-y-6">
<Section title="Identidade">
<Row label="User ID">
<span className="font-mono text-xs truncate">{data.identity.userId}</span>
<CopyButton value={data.identity.userId} />
</Row>
<Row label="Nome">{data.identity.name}</Row>
<Row label="E-mail">{data.identity.email}</Row>
</Section>
<Separator />
<Section title="Sessão">
<Row label="Criada em">{formatDate(data.session.createdAt)}</Row>
<Row label="Expira em">{formatDate(data.session.expiresAt)}</Row>
</Section>
<Separator />
<Section title="Aplicação">
<Row label="Versão">
<span className="font-mono">v{data.app.version}</span>
</Row>
<Row label="Ambiente">
<Badge variant="outline" className="font-mono font-medium">
{data.app.nodeEnv}
</Badge>
</Row>
{data.app.buildSha && (
<Row label="Build SHA">
<span className="font-mono text-xs">{data.app.buildSha.slice(0, 8)}</span>
<CopyButton value={data.app.buildSha} />
</Row>
)}
</Section>
<Separator />
<Section title="Configuração do servidor">
<Row label="Storage S3">
<StatusBadge ok={data.server.s3Configured} />
</Row>
<Row label="E-mail (Resend)">
<StatusBadge ok={data.server.emailConfigured} />
</Row>
<Row label="Domínio público">
{data.server.publicDomain ? (
<span className="font-mono text-xs">{data.server.publicDomain}</span>
) : (
<span className="text-muted-foreground text-xs">Não definido</span>
)}
</Row>
</Section>
<Separator />
<Section title="Saúde">
<Row label="Banco de dados">
{data.health.db === "ok" ? (
<Badge variant="outline" className="gap-1 text-success border-success/30 bg-success/5 font-medium">
<RiWifiLine className="size-3" />
Online
{data.health.dbLatencyMs !== null && (
<span className="text-muted-foreground font-normal">
· {data.health.dbLatencyMs}ms
</span>
)}
</Badge>
) : (
<Badge variant="outline" className="gap-1 text-destructive border-destructive/30 bg-destructive/5 font-medium">
<RiCloseCircleLine className="size-3" />
Erro
</Badge>
)}
</Row>
</Section>
<Separator />
<Section title="Uso">
<Row label="Lançamentos">
<span>{data.usage.transactions.toLocaleString("pt-BR")}</span>
</Row>
<Row label="Anexos">
<span>{data.usage.attachments.toLocaleString("pt-BR")}</span>
</Row>
<Row label="Anotações">
<span>{data.usage.notes.toLocaleString("pt-BR")}</span>
</Row>
<Row label="Itens no Inbox">
<span>{data.usage.inboxItems.toLocaleString("pt-BR")}</span>
</Row>
</Section>
</div>
);
}

View File

@@ -1,96 +0,0 @@
import { count, eq } from "drizzle-orm";
import {
attachments,
inboxItems,
notes,
transactions,
} from "@/db/schema";
import { db } from "@/shared/lib/db";
export type DiagnosticsData = {
identity: {
userId: string;
name: string;
email: string;
};
session: {
createdAt: Date;
expiresAt: Date;
};
app: {
version: string;
nodeEnv: string;
buildSha: string | null;
};
server: {
s3Configured: boolean;
emailConfigured: boolean;
publicDomain: string | null;
};
health: {
db: "ok" | "error";
dbLatencyMs: number | null;
};
usage: {
transactions: number;
attachments: number;
notes: number;
inboxItems: number;
};
};
export async function fetchDiagnosticsData(
userId: string,
user: { id: string; name: string; email: string },
session: { createdAt: Date; expiresAt: Date },
): Promise<DiagnosticsData> {
const dbStart = Date.now();
let dbStatus: "ok" | "error" = "ok";
let dbLatencyMs: number | null = null;
try {
await db.execute("SELECT 1");
dbLatencyMs = Date.now() - dbStart;
} catch {
dbStatus = "error";
}
const [txCount, attachCount, notesCount, inboxCount] = await Promise.all([
db.select({ value: count() }).from(transactions).where(eq(transactions.userId, userId)),
db.select({ value: count() }).from(attachments).where(eq(attachments.userId, userId)),
db.select({ value: count() }).from(notes).where(eq(notes.userId, userId)),
db.select({ value: count() }).from(inboxItems).where(eq(inboxItems.userId, userId)),
]);
return {
identity: {
userId: user.id,
name: user.name,
email: user.email,
},
session: {
createdAt: session.createdAt,
expiresAt: session.expiresAt,
},
app: {
version: process.env.npm_package_version ?? "—",
nodeEnv: process.env.NODE_ENV ?? "—",
buildSha: process.env.BUILD_SHA ?? null,
},
server: {
s3Configured: !!(process.env.S3_ENDPOINT && process.env.S3_ACCESS_KEY_ID),
emailConfigured: !!process.env.RESEND_FROM_EMAIL,
publicDomain: process.env.PUBLIC_DOMAIN ?? null,
},
health: {
db: dbStatus,
dbLatencyMs,
},
usage: {
transactions: txCount[0]?.value ?? 0,
attachments: attachCount[0]?.value ?? 0,
notes: notesCount[0]?.value ?? 0,
inboxItems: inboxCount[0]?.value ?? 0,
},
};
}

View File

@@ -119,11 +119,8 @@ export function NavbarUser({
/>
</div>
<div className="flex flex-col min-w-0">
<span className="text-sm font-medium truncate">{user.name}</span>
<div className="flex items-center gap-1 min-w-0">
<span className="text-xs text-muted-foreground truncate">
{user.email}
</span>
<span className="text-sm font-medium truncate">{user.name}</span>
<Tooltip>
<TooltipTrigger asChild>
<button
@@ -144,6 +141,9 @@ export function NavbarUser({
</TooltipContent>
</Tooltip>
</div>
<span className="text-xs text-muted-foreground truncate">
{user.email}
</span>
</div>
</DropdownMenuLabel>