Pular para o conteúdo
voltar

/snippets

Snippets reutilizáveis

Hooks e patterns que extraí dos meus projetos open-source. Pega, cola, usa em produção. Todos sob MIT — sem precisar atribuir, mas se ajudar, manda um ⭐ no repo de origem.

useNumberTicker

Anima um número de 0 até `value` durante `duration` ms. Respeita prefers-reduced-motion. Ease-out cubic.

OFICINA · src/lib/useNumberTicker.ts
import { useEffect, useState } from 'react';

export function useNumberTicker(value: number, duration = 600): number {
  const [display, setDisplay] = useState(0);

  useEffect(() => {
    if (typeof window === 'undefined') {
      setDisplay(value);
      return;
    }

    const prefersReduced = window.matchMedia(
      '(prefers-reduced-motion: reduce)'
    ).matches;
    if (prefersReduced || value === 0) {
      setDisplay(value);
      return;
    }

    const start = performance.now();
    const startValue = display;
    const delta = value - startValue;
    let frame = 0;

    const step = (now: number) => {
      const elapsed = now - start;
      const t = Math.min(elapsed / duration, 1);
      const eased = 1 - Math.pow(1 - t, 3); // ease-out cubic
      setDisplay(Math.round(startValue + delta * eased));
      if (t < 1) frame = requestAnimationFrame(step);
      else setDisplay(value);
    };

    frame = requestAnimationFrame(step);
    return () => cancelAnimationFrame(frame);
  }, [value, duration]);

  return display;
}

useTheme

Theme hook com 3 estados (light/dark/system) + persistência em localStorage + sincronização com mudança do SO quando system.

OFICINA · src/lib/useTheme.ts
import { useEffect, useState, useCallback } from 'react';

export type Theme = 'light' | 'dark' | 'system';
const STORAGE_KEY = 'app-theme';

function resolveTheme(theme: Theme): 'light' | 'dark' {
  if (theme === 'system') {
    return window.matchMedia('(prefers-color-scheme: dark)').matches
      ? 'dark'
      : 'light';
  }
  return theme;
}

function applyTheme(resolved: 'light' | 'dark') {
  const root = document.documentElement;
  root.classList.toggle('dark', resolved === 'dark');
  root.style.colorScheme = resolved;
}

export function useTheme() {
  const [theme, setThemeState] = useState<Theme>(() => {
    const stored = localStorage.getItem(STORAGE_KEY);
    return (stored === 'light' || stored === 'dark' || stored === 'system')
      ? stored
      : 'system';
  });

  useEffect(() => {
    applyTheme(resolveTheme(theme));
  }, [theme]);

  // Reage a mudança do SO quando theme === 'system'
  useEffect(() => {
    if (theme !== 'system') return;
    const mql = window.matchMedia('(prefers-color-scheme: dark)');
    const handler = () => applyTheme(mql.matches ? 'dark' : 'light');
    mql.addEventListener('change', handler);
    return () => mql.removeEventListener('change', handler);
  }, [theme]);

  const setTheme = useCallback((next: Theme) => {
    localStorage.setItem(STORAGE_KEY, next);
    setThemeState(next);
  }, []);

  return { theme, setTheme, resolvedTheme: resolveTheme(theme) };
}

fetch interceptor (auth global)

Patcha o window.fetch pra injetar Authorization header em todo /api/* + captura 401 dispatching event. Permite multi-tenancy sem refatorar 30 components.

OFICINA · src/lib/api.ts
const TOKEN_KEY = 'app:token';

export function getToken(): string | null {
  return localStorage.getItem(TOKEN_KEY);
}

export function installFetchInterceptor() {
  if (typeof window === 'undefined') return;
  const original = window.fetch.bind(window);

  window.fetch = async (input, init) => {
    const url = typeof input === 'string'
      ? input
      : input instanceof Request ? input.url : input.toString();

    const isApi = url.includes('/api/');
    const isAuthEndpoint = url.endsWith('/api/auth/login')
      || url.endsWith('/api/auth/register');

    if (isApi) {
      const token = getToken();
      const headers = new Headers(init?.headers);
      if (token && !headers.has('Authorization')) {
        headers.set('Authorization', `Bearer ${token}`);
      }
      init = { ...init, headers };
    }

    const response = await original(input, init);

    if (response.status === 401 && isApi && !isAuthEndpoint) {
      localStorage.removeItem(TOKEN_KEY);
      window.dispatchEvent(new CustomEvent('auth:unauthorized'));
    }

    return response;
  };
}

NDJSON streaming reader (frontend)

Lê uma response em streaming linha-a-linha (1 JSON por linha). Alternativa ao EventSource quando você precisa de POST com body.

pr-reviewer · app/page.tsx
async function readNDJSON<T>(
  response: Response,
  onEvent: (event: T) => void
) {
  if (!response.body) throw new Error('No body to stream');

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });

    let newlineIdx = buffer.indexOf('\n');
    while (newlineIdx !== -1) {
      const line = buffer.slice(0, newlineIdx).trim();
      buffer = buffer.slice(newlineIdx + 1);
      if (line) {
        try {
          onEvent(JSON.parse(line) as T);
        } catch (e) {
          console.error('Invalid NDJSON line:', line);
        }
      }
      newlineIdx = buffer.indexOf('\n');
    }
  }
}

NDJSON streaming response (backend)

Cria um ReadableStream que escreve eventos NDJSON. Útil pra route handlers do Next.js quando você quer streaming progressivo.

pr-reviewer · app/api/analyze/stream/route.ts
export async function POST(req: Request) {
  const encoder = new TextEncoder();
  const event = (data: Record<string, unknown>) =>
    encoder.encode(JSON.stringify(data) + '\n');

  const stream = new ReadableStream({
    async start(controller) {
      try {
        controller.enqueue(event({ type: 'meta', /* ... */ }));

        // Stream do LLM / processo longo
        for await (const chunk of someAsyncIterable) {
          controller.enqueue(event({ type: 'chunk', text: chunk }));
        }

        controller.enqueue(event({ type: 'done' }));
        controller.close();
      } catch (err) {
        controller.enqueue(event({
          type: 'error',
          message: err instanceof Error ? err.message : 'unknown',
        }));
        controller.close();
      }
    },
  });

  return new Response(stream, {
    headers: {
      'Content-Type': 'application/x-ndjson',
      'Cache-Control': 'no-cache, no-transform',
      'X-Accel-Buffering': 'no',
    },
  });
}

shortIdFromUrl (deterministic share IDs)

Gera ID curto e estável a partir de uma URL via SHA-256. Mesmo input = mesmo ID. Útil pra share links que se beneficiam de cache compartilhado.

pr-reviewer · lib/redis.ts
import crypto from 'node:crypto';

function normalizeUrl(url: string): string {
  try {
    const u = new URL(url);
    return `${u.host}${u.pathname}`.toLowerCase().replace(/\/$/, '');
  } catch {
    return url.toLowerCase().trim();
  }
}

export function shortIdFromUrl(url: string): string {
  const normalized = normalizeUrl(url);
  const hash = crypto.createHash('sha256').update(normalized).digest('hex');
  return hash.slice(0, 10);
  // 10 chars hex = 40 bits = ~1 trilhão de IDs únicos.
  // Validate no consumo: /^[a-f0-9]{10}$/.test(id)
}

verifySignature (GitHub webhook HMAC)

Valida assinatura HMAC SHA-256 de webhook do GitHub usando crypto.timingSafeEqual (previne timing attacks). Use no telegram-commits-mirror e qualquer integração com webhooks.

telegram-commits-mirror · api/webhook.ts
import crypto from 'node:crypto';

export function verifySignature(
  payload: string,
  signatureHeader: string | undefined,
  secret: string
): boolean {
  if (!signatureHeader) return false;

  const expected =
    'sha256=' +
    crypto.createHmac('sha256', secret).update(payload).digest('hex');

  const a = Buffer.from(signatureHeader);
  const b = Buffer.from(expected);

  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}