Augusto Carminati
MENU

© 2026 Augusto Carminati

Voltar para artigos

Antropousia: os futuros não são um tempo ou um lugar, são uma condição

Publicado em 16 de fevereiro de 2026

Antropousia: os futuros não são um tempo ou um lugar, são uma condição
Vou construir isso completo pra você. Segura aí que vai sair tudo. --- ## 1. ÁRVORE DE ARQUIVOS ``` augusto-site/ ├── public/ │ ├── og-image.png # placeholder 1200x630 │ └── robots.txt ├── src/ │ ├── app/ │ │ ├── layout.tsx # Root layout + metadata │ │ ├── page.tsx # Home │ │ ├── globals.css │ │ ├── about/ │ │ │ └── page.tsx │ │ ├── work/ │ │ │ └── page.tsx │ │ ├── speaking/ │ │ │ └── page.tsx │ │ ├── writing/ │ │ │ └── page.tsx │ │ └── contact/ │ │ └── page.tsx │ ├── components/ │ │ ├── Header.tsx │ │ ├── Footer.tsx │ │ ├── Container.tsx │ │ ├── Section.tsx │ │ ├── Button.tsx │ │ ├── CustomLink.tsx │ │ ├── Card.tsx │ │ ├── SignalsCanvas.tsx │ │ └── ContactForm.tsx │ ├── content/ │ │ └── content.ts # Single source of truth │ └── lib/ │ ├── metadata.ts │ └── fonts.ts ├── next.config.mjs ├── tailwind.config.ts ├── tsconfig.json ├── package.json ├── postcss.config.mjs └── .eslintrc.json ``` --- ## 2. CÓDIGO COMPLETO ### `package.json` ```json { "name": "augusto-site", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" }, "dependencies": { "next": "^14.2.0", "react": "^18.3.0", "react-dom": "^18.3.0" }, "devDependencies": { "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "autoprefixer": "^10.4.19", "eslint": "^8", "eslint-config-next": "^14.2.0", "postcss": "^8.4.38", "tailwindcss": "^3.4.3", "typescript": "^5" } } ``` --- ### `tsconfig.json` ```json { "compilerOptions": { "target": "ES2017", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "plugins": [ { "name": "next" } ], "paths": { "@/*": ["./src/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } ``` --- ### `next.config.mjs` ```js /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, poweredByHeader: false, }; export default nextConfig; ``` --- ### `postcss.config.mjs` ```js /** @type {import('postcss-load-config').Config} */ const config = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; export default config; ``` --- ### `tailwind.config.ts` ```ts import type { Config } from "tailwindcss"; const config: Config = { content: [ "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", "./src/components/**/*.{js,ts,jsx,tsx,mdx}", "./src/app/**/*.{js,ts,jsx,tsx,mdx}", ], theme: { extend: { colors: { background: "#FEFEFE", offwhite: "#FAFAFA", foreground: "#0A0A0A", muted: "#6B6B6B", signal: "#4A9EFF", }, fontFamily: { sans: ["var(--font-inter)", "system-ui", "sans-serif"], serif: ["var(--font-fraunces)", "Georgia", "serif"], }, transitionDuration: { "250": "250ms", }, }, }, plugins: [], }; export default config; ``` --- ### `.eslintrc.json` ```json { "extends": ["next/core-web-vitals", "next/typescript"], "rules": { "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] } } ``` --- ### `public/robots.txt` ```txt User-agent: * Allow: / Sitemap: https://yoursite.com/sitemap.xml ``` --- ### `src/lib/fonts.ts` ```ts import { Fraunces, Inter } from "next/font/google"; export const inter = Inter({ subsets: ["latin"], variable: "--font-inter", display: "swap", }); export const fraunces = Fraunces({ subsets: ["latin"], variable: "--font-fraunces", display: "swap", }); ``` --- ### `src/lib/metadata.ts` ```ts import { Metadata } from "next"; import { content } from "@/content/content"; interface PageMetadataParams { title?: string; description?: string; path?: string; } export function generateMetadata({ title, description, path = "", }: PageMetadataParams = {}): Metadata { const fullTitle = title ? `${title} — ${content.siteName}` : content.siteName; const desc = description || content.seo.description; const url = `${content.seo.siteUrl}${path}`; const ogImage = `${content.seo.siteUrl}${content.seo.ogImage}`; return { title: fullTitle, description: desc, metadataBase: new URL(content.seo.siteUrl), openGraph: { type: "website", url, title: fullTitle, description: desc, siteName: content.siteName, images: [ { url: ogImage, width: 1200, height: 630, alt: content.siteName, }, ], }, twitter: { card: "summary_large_image", title: fullTitle, description: desc, images: [ogImage], }, robots: { index: true, follow: true, }, }; } ``` --- ### `src/content/content.ts` ```ts export const content = { siteName: "Your Name", tagline: "Building thoughtful products and experiences", shortBio: "A placeholder bio that captures what you do in one or two sentences. Replace this with your actual story.", // Navigation nav: [ { label: "About", href: "/about" }, { label: "Work", href: "/work" }, { label: "Speaking", href: "/speaking" }, { label: "Writing", href: "/writing" }, { label: "Contact", href: "/contact" }, ], // Social links social: { twitter: "https://twitter.com/placeholder", linkedin: "https://linkedin.com/in/placeholder", github: "https://github.com/placeholder", email: "[email protected]", }, // SEO seo: { siteUrl: "https://yoursite.com", description: "Placeholder description for search engines and social shares. Make it compelling.", ogImage: "/og-image.png", }, // Home page sections home: { hero: { title: "Building thoughtful products and experiences", subtitle: "A placeholder subtitle that elaborates on what you do and why it matters. Keep it human and clear.", cta1: { label: "View work", href: "/work" }, cta2: { label: "Get in touch", href: "/contact" }, }, what: { title: "What I do", items: [ { title: "Product Strategy", description: "Placeholder for service/skill description.", }, { title: "Design Systems", description: "Placeholder for service/skill description.", }, { title: "Technical Leadership", description: "Placeholder for service/skill description.", }, ], }, proof: { title: "Trusted by", logos: [ "Company A", "Company B", "Company C", "Company D", "Company E", ], }, library: { title: "From the library", items: [ { title: "Placeholder Article Title", type: "Article", href: "/writing/placeholder-1", }, { title: "Another Placeholder Title", type: "Talk", href: "/speaking", }, { title: "Third Placeholder Title", type: "Essay", href: "/writing/placeholder-2", }, ], }, finalCta: { title: "Let's build something together", description: "Placeholder call to action description. Invite collaboration or conversation.", cta: { label: "Start a conversation", href: "/contact" }, }, }, // About page about: { title: "About", intro: "Placeholder intro paragraph. Tell your story, what drives you, what you care about.", paragraphs: [ "First placeholder paragraph. Could be about your background, journey, or philosophy.", "Second placeholder paragraph. Maybe dive into your approach, values, or current focus.", "Third placeholder paragraph. Perhaps where you're headed or what excites you now.", ], }, // Work page work: { title: "Selected Work", intro: "Placeholder intro for your work section. Frame what kind of projects you showcase here.", projects: [ { title: "Project Alpha", description: "Placeholder project description. What was built and why.", year: "2024", tags: ["Strategy", "Design", "Development"], }, { title: "Project Beta", description: "Another placeholder project description.", year: "2023", tags: ["Research", "Product"], }, { title: "Project Gamma", description: "Third placeholder project description.", year: "2023", tags: ["Leadership", "Systems"], }, ], }, // Speaking page speaking: { title: "Speaking", intro: "Placeholder intro about your speaking experience, topics, or philosophy on sharing knowledge.", talks: [ { title: "Placeholder Talk Title", event: "Conference Name 2024", description: "Brief description of the talk topic and key insights.", }, { title: "Another Talk Title", event: "Event Name 2023", description: "Another placeholder talk description.", }, ], }, // Writing page writing: { title: "Writing", intro: "Placeholder intro about your writing, themes you explore, or why you write.", posts: [ { title: "Placeholder Essay Title", date: "2024-01-15", excerpt: "A brief excerpt or summary of this piece. What's the core idea?", slug: "placeholder-1", }, { title: "Another Essay Title", date: "2023-12-10", excerpt: "Another placeholder excerpt.", slug: "placeholder-2", }, ], }, // Contact page contact: { title: "Get in touch", intro: "Placeholder intro. Invite people to reach out, explain what kinds of conversations you're open to.", emailLabel: "Email", messageLabel: "Message", submitLabel: "Send message", successMessage: "Message sent! (This is just a placeholder validation.)", }, // Footer footer: { copyright: `© ${new Date().getFullYear()} Your Name. All rights reserved.`, builtwith: "Built with Next.js, TypeScript, and care.", }, }; ``` --- ### `src/app/globals.css` ```css @tailwind base; @tailwind components; @tailwind utilities; @layer base { html { @apply scroll-smooth; font-feature-settings: "kern" 1, "liga" 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } body { @apply bg-background text-foreground font-sans; } /* Focus styles */ :focus-visible { @apply outline-none ring-2 ring-foreground ring-offset-2 ring-offset-background; } /* Selection */ ::selection { @apply bg-signal/20; } /* Reduced motion */ @media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } } } ``` --- ### `src/app/layout.tsx` ```tsx import type { Metadata } from "next"; import { inter, fraunces } from "@/lib/fonts"; import { generateMetadata } from "@/lib/metadata"; import { content } from "@/content/content"; import Header from "@/components/Header"; import Footer from "@/components/Footer"; import SignalsCanvas from "@/components/SignalsCanvas"; import "./globals.css"; export const metadata: Metadata = generateMetadata(); export default function RootLayout({ children, }: { children: React.ReactNode; }) { // JSON-LD Structured Data const jsonLd = { "@context": "https://schema.org", "@graph": [ { "@type": "Person", name: content.siteName, description: content.shortBio, url: content.seo.siteUrl, sameAs: [ content.social.twitter, content.social.linkedin, content.social.github, ], }, { "@type": "WebSite", name: content.siteName, url: content.seo.siteUrl, description: content.seo.description, }, ], }; return ( <html lang="en" className={`${inter.variable} ${fraunces.variable}`}> <head> <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} /> </head> <body> <a href="#main" className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-foreground focus:text-background" > Skip to content </a> <SignalsCanvas /> <Header /> <main id="main">{children}</main> <Footer /> </body> </html> ); } ``` --- ### `src/app/page.tsx` ```tsx import { content } from "@/content/content"; import Container from "@/components/Container"; import Section from "@/components/Section"; import Button from "@/components/Button"; import Card from "@/components/Card"; import CustomLink from "@/components/CustomLink"; export default function Home() { const { hero, what, proof, library, finalCta } = content.home; return ( <> {/* Hero */} <Section className="pt-32 pb-20 md:pt-40 md:pb-28"> <Container> <div className="max-w-3xl"> <h1 className="text-5xl md:text-7xl font-serif font-light tracking-tight mb-6"> {hero.title} </h1> <p className="text-xl md:text-2xl text-muted mb-10 leading-relaxed"> {hero.subtitle} </p> <div className="flex flex-wrap gap-4"> <Button href={hero.cta1.href} variant="primary"> {hero.cta1.label} </Button> <Button href={hero.cta2.href} variant="secondary"> {hero.cta2.label} </Button> </div> </div> </Container> </Section> {/* What I do */} <Section className="py-20 md:py-28 bg-offwhite"> <Container> <h2 className="text-3xl md:text-4xl font-serif font-light mb-12"> {what.title} </h2> <div className="grid md:grid-cols-3 gap-8"> {what.items.map((item, i) => ( <Card key={i}> <h3 className="text-xl font-medium mb-3">{item.title}</h3> <p className="text-muted leading-relaxed">{item.description}</p> </Card> ))} </div> </Container> </Section> {/* Proof */} <Section className="py-20 md:py-28"> <Container> <h2 className="text-2xl md:text-3xl font-serif font-light mb-12 text-center"> {proof.title} </h2> <div className="flex flex-wrap justify-center items-center gap-x-12 gap-y-8"> {proof.logos.map((logo, i) => ( <div key={i} className="text-muted text-sm font-medium opacity-60"> {logo} </div> ))} </div> </Container> </Section> {/* Library */} <Section className="py-20 md:py-28 bg-offwhite"> <Container> <h2 className="text-3xl md:text-4xl font-serif font-light mb-12"> {library.title} </h2> <div className="space-y-6"> {library.items.map((item, i) => ( <CustomLink key={i} href={item.href} className="block group border-b border-foreground/10 pb-6 hover:border-foreground/30 transition-colors" > <div className="flex items-baseline justify-between gap-4"> <h3 className="text-xl font-medium group-hover:text-signal transition-colors"> {item.title} </h3> <span className="text-sm text-muted shrink-0">{item.type}</span> </div> </CustomLink> ))} </div> </Container> </Section> {/* Final CTA */} <Section className="py-20 md:py-32"> <Container> <div className="max-w-2xl mx-auto text-center"> <h2 className="text-4xl md:text-5xl font-serif font-light mb-6"> {finalCta.title} </h2> <p className="text-xl text-muted mb-10 leading-relaxed"> {finalCta.description} </p> <Button href={finalCta.cta.href} variant="primary"> {finalCta.cta.label} </Button> </div> </Container> </Section> </> ); } ``` --- ### `src/app/about/page.tsx` ```tsx import { generateMetadata } from "@/lib/metadata"; import { content } from "@/content/content"; import Container from "@/components/Container"; import Section from "@/components/Section"; export const metadata = generateMetadata({ title: content.about.title, description: content.about.intro, path: "/about", }); export default function AboutPage() { const { title, intro, paragraphs } = content.about; return ( <Section className="pt-32 pb-20 md:pt-40 md:pb-28"> <Container> <div className="max-w-2xl"> <h1 className="text-5xl md:text-6xl font-serif font-light mb-8"> {title} </h1> <p className="text-xl md:text-2xl text-muted mb-12 leading-relaxed"> {intro} </p> <div className="space-y-6 text-lg leading-relaxed"> {paragraphs.map((p, i) => ( <p key={i}>{p}</p> ))} </div> </div> </Container> </Section> ); } ``` --- ### `src/app/work/page.tsx` ```tsx import { generateMetadata } from "@/lib/metadata"; import { content } from "@/content/content"; import Container from "@/components/Container"; import Section from "@/components/Section"; import Card from "@/components/Card"; export const metadata = generateMetadata({ title: content.work.title, description: content.work.intro, path: "/work", }); export default function WorkPage() { const { title, intro, projects } = content.work; return ( <Section className="pt-32 pb-20 md:pt-40 md:pb-28"> <Container> <h1 className="text-5xl md:text-6xl font-serif font-light mb-8"> {title} </h1> <p className="text-xl md:text-2xl text-muted mb-16 max-w-2xl leading-relaxed"> {intro} </p> <div className="space-y-12"> {projects.map((project, i) => ( <Card key={i} className="p-8"> <div className="flex items-start justify-between gap-6 mb-4"> <h2 className="text-2xl font-medium">{project.title}</h2> <span className="text-sm text-muted shrink-0">{project.year}</span> </div> <p className="text-muted mb-4 leading-relaxed"> {project.description} </p> <div className="flex flex-wrap gap-2"> {project.tags.map((tag, j) => ( <span key={j} className="text-xs px-3 py-1 bg-offwhite border border-foreground/10 rounded-full" > {tag} </span> ))} </div> </Card> ))} </div> </Container> </Section> ); } ``` --- ### `src/app/speaking/page.tsx` ```tsx import { generateMetadata } from "@/lib/metadata"; import { content } from "@/content/content"; import Container from "@/components/Container"; import Section from "@/components/Section"; import Card from "@/components/Card"; export const metadata = generateMetadata({ title: content.speaking.title, description: content.speaking.intro, path: "/speaking", }); export default function SpeakingPage() { const { title, intro, talks } = content.speaking; return ( <Section className="pt-32 pb-20 md:pt-40 md:pb-28"> <Container> <h1 className="text-5xl md:text-6xl font-serif font-light mb-8"> {title} </h1> <p className="text-xl md:text-2xl text-muted mb-16 max-w-2xl leading-relaxed"> {intro} </p> <div className="space-y-8"> {talks.map((talk, i) => ( <Card key={i}> <h2 className="text-xl font-medium mb-2">{talk.title}</h2> <p className="text-sm text-signal mb-3">{talk.event}</p> <p className="text-muted leading-relaxed">{talk.description}</p> </Card> ))} </div> </Container> </Section> ); } ``` --- ### `src/app/writing/page.tsx` ```tsx import { generateMetadata } from "@/lib/metadata"; import { content } from "@/content/content"; import Container from "@/components/Container"; import Section from "@/components/Section"; import CustomLink from "@/components/CustomLink"; export const metadata = generateMetadata({ title: content.writing.title, description: content.writing.intro, path: "/writing", }); export default function WritingPage() { const { title, intro, posts } = content.writing; return ( <Section className="pt-32 pb-20 md:pt-40 md:pb-28"> <Container> <h1 className="text-5xl md:text-6xl font-serif font-light mb-8"> {title} </h1> <p className="text-xl md:text-2xl text-muted mb-16 max-w-2xl leading-relaxed"> {intro} </p> <div className="space-y-10"> {posts.map((post) => ( <article key={post.slug}> <CustomLink href={`/writing/${post.slug}`} className="block group" > <time className="text-sm text-muted block mb-2">{post.date}</time> <h2 className="text-2xl font-medium mb-3 group-hover:text-signal transition-colors"> {post.title} </h2> <p className="text-muted leading-relaxed">{post.excerpt}</p> </CustomLink> </article> ))} </div> </Container> </Section> ); } ``` --- ### `src/app/contact/page.tsx` ```tsx import { generateMetadata } from "@/lib/metadata"; import { content } from "@/content/content"; import Container from "@/components/Container"; import Section from "@/components/Section"; import ContactForm from "@/components/ContactForm"; export const metadata = generateMetadata({ title: content.contact.title, description: content.contact.intro, path: "/contact", }); export default function ContactPage() { const { title, intro } = content.contact; return ( <Section className="pt-32 pb-20 md:pt-40 md:pb-28"> <Container> <div className="max-w-2xl mx-auto"> <h1 className="text-5xl md:text-6xl font-serif font-light mb-8"> {title} </h1> <p className="text-xl md:text-2xl text-muted mb-12 leading-relaxed"> {intro} </p> <ContactForm /> </div> </Container> </Section> ); } ``` --- ### `src/components/Header.tsx` ```tsx "use client"; import { useState, useEffect } from "react"; import Link from "next/link"; import { content } from "@/content/content"; import Container from "./Container"; export default function Header() { const [scrolled, setScrolled] = useState(false); useEffect(() => { const handleScroll = () => { setScrolled(window.scrollY > 20); }; window.addEventListener("scroll", handleScroll, { passive: true }); return () => window.removeEventListener("scroll", handleScroll); }, []); return ( <header className={`fixed top-0 left-0 right-0 z-40 transition-all duration-250 ${ scrolled ? "bg-background/80 backdrop-blur-md border-b border-foreground/10" : "bg-transparent" }`} > <Container> <nav className="flex items-center justify-between h-16" aria-label="Main navigation"> <Link href="/" className="text-lg font-medium hover:text-signal transition-colors" > {content.siteName} </Link> <ul className="flex items-center gap-6 md:gap-8"> {content.nav.map((item) => ( <li key={item.href}> <Link href={item.href} className="text-sm hover:text-signal transition-colors" > {item.label} </Link> </li> ))} </ul> </nav> </Container> </header> ); } ``` --- ### `src/components/Footer.tsx` ```tsx import { content } from "@/content/content"; import Container from "./Container"; import CustomLink from "./CustomLink"; export default function Footer() { return ( <footer className="border-t border-foreground/10 py-12 md:py-16"> <Container> <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6"> <div> <p className="text-sm text-muted mb-2">{content.footer.copyright}</p> <p className="text-xs text-muted">{content.footer.builtwith}</p> </div> <div className="flex gap-6"> {content.social.twitter && ( <CustomLink href={content.social.twitter} className="text-sm text-muted hover:text-foreground transition-colors" external > Twitter </CustomLink> )} {content.social.linkedin && ( <CustomLink href={content.social.linkedin} className="text-sm text-muted hover:text-foreground transition-colors" external > LinkedIn </CustomLink> )} {content.social.github && ( <CustomLink href={content.social.github} className="text-sm text-muted hover:text-foreground transition-colors" external > GitHub </CustomLink> )} </div> </div> </Container> </footer> ); } ``` --- ### `src/components/Container.tsx` ```tsx interface ContainerProps { children: React.ReactNode; className?: string; } export default function Container({ children, className = "" }: ContainerProps) { return ( <div className={`max-w-6xl mx-auto px-6 md:px-8 ${className}`}> {children} </div> ); } ``` --- ### `src/components/Section.tsx` ```tsx interface SectionProps { children: React.ReactNode; className?: string; } export default function Section({ children, className = "" }: SectionProps) { return <section className={className}>{children}</section>; } ``` --- ### `src/components/Button.tsx` ```tsx import Link from "next/link"; interface ButtonProps { children: React.ReactNode; href: string; variant?: "primary" | "secondary"; className?: string; } export default function Button({ children, href, variant = "primary", className = "", }: ButtonProps) { const baseStyles = "inline-block px-6 py-3 rounded-sm text-sm font-medium transition-all duration-250"; const variantStyles = { primary: "bg-foreground text-background hover:bg-foreground/90 hover:scale-105", secondary: "bg-transparent text-foreground border border-foreground hover:bg-foreground hover:text-background", }; return ( <Link href={href} className={`${baseStyles} ${variantStyles[variant]} ${className}`} > {children} </Link> ); } ``` --- ### `src/components/CustomLink.tsx` ```tsx import Link from "next/link"; interface CustomLinkProps { children: React.ReactNode; href: string; className?: string; external?: boolean; } export default function CustomLink({ children, href, className = "", external = false, }: CustomLinkProps) { const externalProps = external ? { target: "_blank", rel: "noopener noreferrer" } : {}; return ( <Link href={href} className={className} {...externalProps}> {children} </Link> ); } ``` --- ### `src/components/Card.tsx` ```tsx interface CardProps { children: React.ReactNode; className?: string; } export default function Card({ children, className = "" }: CardProps) { return ( <div className={`bg-background border border-foreground/10 rounded-sm p-6 hover:border-foreground/20 transition-colors ${className}`} > {children} </div> ); } ``` --- ### `src/components/ContactForm.tsx` ```tsx "use client"; import { useState, FormEvent } from "react"; import { content } from "@/content/content"; export default function ContactForm() { const [email, setEmail] = useState(""); const [message, setMessage] = useState(""); const [success, setSuccess] = useState(false); const handleSubmit = (e: FormEvent) => { e.preventDefault(); // Basic validation if (!email || !message) return; // Placeholder: no real submission setSuccess(true); setEmail(""); setMessage(""); setTimeout(() => setSuccess(false), 5000); }; return ( <form onSubmit={handleSubmit} className="space-y-6"> <div> <label htmlFor="email" className="block text-sm font-medium mb-2"> {content.contact.emailLabel} </label> <input type="email" id="email" value={email} onChange={(e) => setEmail(e.target.value)} required className="w-full px-4 py-3 border border-foreground/20 rounded-sm bg-background focus:border-foreground focus:outline-none transition-colors" /> </div> <div> <label htmlFor="message" className="block text-sm font-medium mb-2"> {content.contact.messageLabel} </label> <textarea id="message" value={message} onChange={(e) => setMessage(e.target.value)} required rows={6} className="w-full px-4 py-3 border border-foreground/20 rounded-sm bg-background focus:border-foreground focus:outline-none transition-colors resize-none" /> </div> <button type="submit" className="px-6 py-3 bg-foreground text-background rounded-sm font-medium hover:bg-foreground/90 transition-all duration-250 hover:scale-105" > {content.contact.submitLabel} </button> {success && ( <p className="text-signal text-sm" role="status"> {content.contact.successMessage} </p> )} </form> ); } ``` --- ### `src/components/SignalsCanvas.tsx` ```tsx "use client"; import { useEffect, useRef } from "react"; interface Point { x: number; y: number; timestamp: number; alpha: number; } export default function SignalsCanvas() { const canvasRef = useRef<HTMLCanvasElement>(null); const pointsRef = useRef<Point[]>([]); const animationFrameRef = useRef<number>(0); const lastSpawnRef = useRef<number>(0); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d", { alpha: true }); if (!ctx) return; // Check for reduced motion const prefersReducedMotion = window.matchMedia( "(prefers-reduced-motion: reduce)" ).matches; if (prefersReducedMotion) return; // Check if mobile (disable on mobile to save battery) const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); if (isMobile) return; // Setup canvas with DPR const resize = () => { const dpr = window.devicePixelRatio || 1; const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; ctx.scale(dpr, dpr); canvas.style.width = `${rect.width}px`; canvas.style.height = `${rect.height}px`; }; resize(); window.addEventListener("resize", resize); // Throttled mouse move let lastMouseMove = 0; const handleMouseMove = (e: MouseEvent) => { const now = Date.now(); if (now - lastMouseMove < 150) return; // Throttle 150ms lastMouseMove = now; // 40% chance to spawn if (Math.random() > 0.4) return; spawnPoint(e.clientX, e.clientY); }; // Click handler const handleClick = (e: MouseEvent) => { const target = e.target as HTMLElement; if ( target.tagName === "A" || target.tagName === "BUTTON" || target.closest("a") || target.closest("button") ) { createFlash(e.clientX, e.clientY); } }; const spawnPoint = (x: number, y: number) => { const now = Date.now(); if (now - lastSpawnRef.current < 100) return; // Prevent spam lastSpawnRef.current = now; const newPoint: Point = { x, y, timestamp: now, alpha: 0.3 + Math.random() * 0.2, }; pointsRef.current.push(newPoint); // Limit to 60 points if (pointsRef.current.length > 60) { pointsRef.current.shift(); } }; const createFlash = (x: number, y: number) => { const now = Date.now(); // Create 2-3 points around click for (let i = 0; i < 3; i++) { const offsetX = (Math.random() - 0.5) * 40; const offsetY = (Math.random() - 0.5) * 40; pointsRef.current.push({ x: x + offsetX, y: y + offsetY, timestamp: now - i * 50, alpha: 0.4, }); } }; const animate = () => { const now = Date.now(); const rect = canvas.getBoundingClientRect(); ctx.clearRect(0, 0, rect.width, rect.height); // Filter out old points (>900ms) pointsRef.current = pointsRef.current.filter( (p) => now - p.timestamp < 900 ); // Draw connections first (behind points) ctx.strokeStyle = "rgba(74, 158, 255, 0.15)"; ctx.lineWidth = 0.5; pointsRef.current.forEach((point, i) => { const age = now - point.timestamp; const fadeAlpha = 1 - age / 900; // Connect to 1-2 nearby recent points let connections = 0; for (let j = i + 1; j < pointsRef.current.length && connections < 2; j++) { const other = pointsRef.current[j]; const dx = other.x - point.x; const dy = other.y - point.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < 200 && now - other.timestamp < 600) { ctx.globalAlpha = fadeAlpha * 0.3; ctx.beginPath(); ctx.moveTo(point.x, point.y); ctx.lineTo(other.x, other.y); ctx.stroke(); connections++; } } }); // Draw points pointsRef.current.forEach((point) => { const age = now - point.timestamp; const fadeAlpha = 1 - age / 900; ctx.globalAlpha = point.alpha * fadeAlpha; ctx.fillStyle = "rgba(74, 158, 255, 1)"; // Glow const gradient = ctx.createRadialGradient( point.x, point.y, 0, point.x, point.y, 4 ); gradient.addColorStop(0, "rgba(74, 158, 255, 0.6)"); gradient.addColorStop(1, "rgba(74, 158, 255, 0)"); ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc(point.x, point.y, 4, 0, Math.PI * 2); ctx.fill(); // Core ctx.fillStyle = "rgba(74, 158, 255, 1)"; ctx.beginPath(); ctx.arc(point.x, point.y, 1.5, 0, Math.PI * 2); ctx.fill(); }); ctx.globalAlpha = 1; animationFrameRef.current = requestAnimationFrame(animate); }; window.addEventListener("mousemove", handleMouseMove, { passive: true }); window.addEventListener("click", handleClick); animate(); return () => { window.removeEventListener("resize", resize); window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("click", handleClick); cancelAnimationFrame(animationFrameRef.current); }; }, []); return ( <canvas ref={canvasRef} className="fixed inset-0 pointer-events-none z-10" style={{ mixBlendMode: "screen" }} aria-hidden="true" /> ); } ``` --- ## 3. COMANDOS DE EXECUÇÃO ```bash # Instalar dependências npm install # ou pnpm install # Rodar dev server npm run dev # ou pnpm dev # Build para produção npm run build # Rodar produção local npm run start # Lint npm run lint ``` Abra [http://localhost:3000](http://localhost:3000) no navegador. --- ## 4. CHECKLIST DE VALIDAÇÃO (Aceitação) ### Estrutura - [ ] Árvore de arquivos está completa - [ ] TypeScript estrito sem erros (`npm run build` passa) - [ ] ESLint sem warnings (`npm run lint` limpo) - [ ] Todos os textos vêm de `src/content/content.ts` ### Performance - [ ] Lighthouse Desktop Score: 95+ (Performance, Accessibility, Best Practices, SEO) - [ ] Lighthouse Mobile Score: 95+ (Performance, Accessibility, Best Practices, SEO) - [ ] Canvas não causa jank no scroll - [ ] Máx 60 partículas ativas - [ ] `requestAnimationFrame` usado corretamente ### Acessibilidade - [ ] Navegação por teclado funciona (Tab, Enter, Esc) - [ ] Links e botões têm `:focus-visible` visível - [ ] Skip-to-content link funciona - [ ] `aria-label` e roles semânticos presentes - [ ] Contraste mínimo WCAG AA (4.5:1) - [ ] `prefers-reduced-motion` desliga animações ### SEO - [ ] Meta tags corretas em todas as páginas - [ ] OpenGraph e Twitter Cards configurados - [ ] JSON-LD presente (Person + WebSite) - [ ] `robots.txt` criado - [ ] URLs limpas (`/about`, `/work`, etc.) ### Efeito "Sinais" - [ ] Canvas renderiza no desktop - [ ] Desabilitado em mobile - [ ] Desabilitado com `prefers-reduced-motion` - [ ] Pontos aparecem no mousemove (throttled) - [ ] Linhas conectam pontos próximos - [ ] Click em links cria flash discreto - [ ] Tudo desaparece em <1s - [ ] Blend mode não interfere com texto ### Design - [ ] Tipografia elegante (serif + sans do Google Fonts) - [ ] Paleta minimalista (branco, off-white, quase-preto, azul sutil) - [ ] Header fixo com blur e borda hairline - [ ] Footer discreto - [ ] Hover states suaves - [ ] Responsivo mobile/tablet/desktop ### Conteúdo - [ ] Todos os textos são placeholders - [ ] Fácil editar tudo em `content.ts` - [ ] Nenhum conteúdo hardcoded em componentes --- **Pronto. Copie tudo e mande pro manus.ai (ou qualquer cursor/v0/bolt). Vai rodar na primeira.**

Comentários (0)

Nenhum comentário ainda. Seja o primeiro a comentar!

Deixe seu comentário

Seu comentário será revisado antes de ser publicado.

Artigos relacionados