voltar ~/portfolio

$ open projects/dev-roast

AI code review | produto compartilhável

Dev-roast pega revisão de código com IA e deixa o resultado fácil de compartilhar.

O usuário cola um snippet e recebe crítica técnica, score, veredito, sugestões e uma página de resultado. Por baixo, o projeto usa server-first, tRPC, Drizzle e syntax highlight no servidor.

Server-firstmenos JavaScript no cliente
Type-safeDrizzle -> tRPC -> React
Shareableresultado pronto para compartilhar
Shiki SSRhighlight sem pesar no cliente

$ cat story.md

História

O Dev-roast parte de uma ação simples: colar um trecho de código. A graça está no tom, mas o produto não depende só da piada. Ele entrega análise, pontuação, veredito, sugestões e uma página final que funciona bem como conteúdo compartilhável.

$ cat architecture.svg

Arquitetura

Diagrama da arquitetura do Dev-roast com Client, Next.js App Router, tRPC Router, Gemini e PostgreSQL.
Fluxo server-first: páginas e Server Components preparam dados via tRPC, mutações passam por API Route, a análise usa Gemini e os resultados ficam em PostgreSQL com Drizzle ORM.

$ grep selling-points

Por que vale olhar

Tom que chama atenção

O roast tira a análise de código do formato genérico e deixa o resultado mais fácil de lembrar.

Stack atual

Next.js 16, React 19, tRPC, TanStack Query, Drizzle e Tailwind v4.

Menos peso no cliente

Server Components, prefetch/hydration e Shiki no servidor reduzem trabalho no browser.

$ view code --tabs

Viewer de código

import "server-only";

import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query";
import type { TRPCQueryOptions } from "@trpc/tanstack-react-query";
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
import { cache } from "react";
import { createTRPCContext } from "./init";
import { makeQueryClient } from "./query-client";
import { appRouter } from "./routers/_app";

export const getQueryClient = cache(makeQueryClient);

export const trpc = createTRPCOptionsProxy({
  ctx: createTRPCContext,
  router: appRouter,
  queryClient: getQueryClient,
});

export const caller = appRouter.createCaller(createTRPCContext);

export function HydrateClient({ children }: { children: React.ReactNode }) {
  const queryClient = getQueryClient();
  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      {children}
    </HydrationBoundary>
  );
}

export function prefetch<T extends ReturnType<TRPCQueryOptions<any>>>(
  queryOptions: T,
) {
  const queryClient = getQueryClient();
  if (queryOptions.queryKey[1]?.type === "infinite") {
    return queryClient.prefetchInfiniteQuery(queryOptions as any);
  }
  return queryClient.prefetchQuery(queryOptions);
}
create: baseProcedure
  .input(
    z.object({
      code: z.string().min(1),
      language: z.string(),
      roastMode: z.boolean(),
    })
  )
  .mutation(async ({ ctx, input }) => {
    const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
    const model = genAI.getGenerativeModel({
      model: "gemini-2.0-flash-lite",
      generationConfig: { responseMimeType: "application/json" },
    });

    const result = await model.generateContent(prompt);
    const data = JSON.parse(result.response.text());

    const [roast] = await ctx.db.transaction(async (tx) => {
      const [insertedRoast] = await tx
        .insert(roasts)
        .values({
          code: input.code,
          language: input.language,
          lineCount: input.code.split("\n").length,
          score: data.score,
          verdict: data.verdict,
          roastQuote: data.roastQuote,
          suggestedFix: data.suggestedFix,
        })
        .returning();

      if (data.analysisItems?.length > 0) {
        await tx.insert(analysisItems).values(
          data.analysisItems.map((item, index) => ({
            roastId: insertedRoast.id,
            severity: item.severity,
            title: item.title,
            description: item.description,
            order: index,
          }))
        );
      }

      return [insertedRoast];
    });

    return { id: roast.id };
  }),
/**
 * Server Component: syntax highlighting com zero bundle no client.
 */
export async function CodeBlockContent({
  code,
  lang = "typescript",
  className,
}: CodeBlockContentProps) {
  "use cache";

  const html = await codeToHtml(code, {
    lang,
    theme: "vesper",
  });

  const linesCount = code.trim().split("\n").length;

  return (
    <div className={body({ className })}>
      <div className={lineNumbers()}>
        {Array.from({ length: linesCount }).map((_, i) => (
          <span key={i}>{i + 1}</span>
        ))}
      </div>
      <div
        className={contentStyle()}
        dangerouslySetInnerHTML={{ __html: html }}
      />
    </div>
  );
}
{/* Editor Layers */}
<div className="flex-1 relative overflow-hidden bg-bg-input">
  {/* Layer 1: Código com highlight (visual, não-interativo) */}
  <div
    ref={preRef}
    className="absolute inset-0 p-4 font-mono text-[12px] leading-5 
               pointer-events-none overflow-hidden whitespace-pre"
    dangerouslySetInnerHTML={{ __html: highlightedHtml }}
    aria-hidden="true"
  />

  {/* Layer 2: Textarea transparente (interativo, invisível) */}
  <textarea
    ref={textareaRef}
    value={code}
    onChange={(e) => onChange(e.target.value)}
    onScroll={handleScroll}
    onKeyDown={handleKeyDown}
    spellCheck={false}
    className="absolute inset-0 w-full h-full p-4 bg-transparent 
               text-transparent caret-white font-mono text-[12px] 
               leading-5 resize-none outline-none z-20"
    placeholder="// paste your code here..."
  />
</div>
export const alt = "DevRoast - AI Code Analysis Result";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";

export default async function Image({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const roastData = await caller.roast.getById({ id });

  const getScoreColor = (score: number) => {
    if (score <= 3.5) return "#FBBF24";
    if (score <= 6) return "#EAB308";
    if (score <= 8) return "#22C55E";
    return "#10B981";
  };

  return new ImageResponse(
    (
      <div style={{
        height: "100%", width: "100%",
        display: "flex", flexDirection: "column",
        alignItems: "center", justifyContent: "center",
        backgroundColor: "#000000", padding: "80px",
      }}>
        <span style={{
          fontSize: "160px", fontWeight: 800,
          color: getScoreColor(roastData.score),
        }}>
          {roastData.score.toFixed(1)}
        </span>
        <span style={{ color: "#71717A", fontSize: "48px" }}>/10</span>
        <span style={{ color: "#FFFFFF", fontSize: "24px", fontStyle: "italic" }}>
          "{roastData.roastQuote}"
        </span>
      </div>
    ),
    { ...size }
  );
}

$ result

Uma ideia simples bem fechada: IA para revisar código, página compartilhável e arquitetura server-first por baixo.