$ 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.
$ 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
$ 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.