{"id":174,"date":"2026-09-16T17:50:48","date_gmt":"2026-09-16T20:50:48","guid":{"rendered":"https:\/\/aqgoes.com\/blog\/?p=174"},"modified":"2026-09-16T17:50:48","modified_gmt":"2026-09-16T20:50:48","slug":"logica-javascript-interatividade-portfolio","status":"publish","type":"post","link":"https:\/\/aqgoes.com\/blog\/logica-javascript-interatividade-portfolio\/","title":{"rendered":"Como Programei a L\u00f3gica JavaScript do Meu Portf\u00f3lio"},"content":{"rendered":"\n<h3 class=\"wp-block-heading\">Intradu\u00e7\u00e3o<\/h3>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<h3 class=\"wp-block-heading\">Como Programei a L\u00f3gica JavaScript do Meu Portf\u00f3lio: Menu, Dark Mode e Anima\u00e7\u00f5es<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Depois de montar toda a estrutura sem\u00e2ntica em HTML5 e estilizar o projeto com Tailwind CSS no post anterior, chegou a hora de dar vida ao meu portf\u00f3lio (<strong>aqgoes.com<\/strong>).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Uma interface moderna exige interatividade r\u00e1pida, acessibilidade e micro-intera\u00e7\u00f5es fluidas. Tudo isso precisa rodar sem depender de frameworks pesados como React ou Vue para tarefas simples do client-side.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Neste artigo, vou explicar o c\u00f3digo em <strong>JavaScript Vanilla<\/strong> que desenvolvi para controlar quatro pilares do meu site: o menu mobile din\u00e2mico, o alternador de Dark\/Light Mode com a View Transitions API, o efeito Scroll-Spy de navega\u00e7\u00e3o e as anima\u00e7\u00f5es de Scroll Reveal.<\/p>\n<\/blockquote>\n\n\n\n<h2 class=\"wp-block-heading\">O C\u00f3digo JavaScript Completo do Projeto<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Abaixo est\u00e1 o script principal que roda no projeto, estruturado dentro do evento <code>DOMContentLoaded<\/code> e considerando a API <code>prefers-color-scheme<\/code> e acessibilidade reduzida:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>document.addEventListener('DOMContentLoaded', () => {\n\n    const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n    \/\/ --- 1. L\u00d3GICA DO MENU HAMB\u00daRGUER (abertura\/fechamento suave) ---\n    const menuBtn = document.getElementById('menu-btn');\n    const menu = document.getElementById('menu');\n    const iconOpen = document.getElementById('icon-open');\n    const iconClose = document.getElementById('icon-close');\n    const navLinks = document.querySelectorAll('.nav-link');\n\n    function toggleMenu(forceClose = false) {\n        const isOpen = menu.classList.contains('menu-open');\n        const willOpen = forceClose ? false : !isOpen;\n\n        menu.classList.toggle('menu-open', willOpen);\n        iconOpen.classList.toggle('hidden', willOpen);\n        iconClose.classList.toggle('hidden', !willOpen);\n        menuBtn.setAttribute('aria-expanded', String(willOpen));\n    }\n\n    menuBtn.addEventListener('click', () => toggleMenu());\n\n    navLinks.forEach(link => {\n        link.addEventListener('click', () => {\n            if (window.innerWidth &lt; 768) {\n                toggleMenu(true);\n            }\n        });\n    });\n\n    \/\/ Fecha o menu mobile se a tela for redimensionada para desktop\n    window.addEventListener('resize', () => {\n        if (window.innerWidth >= 768) {\n            toggleMenu(true);\n        }\n    });\n\n    \/\/ --- 2. L\u00d3GICA DO DARK \/ LIGHT MODE (com View Transitions API) ---\n    const themeToggles = document.querySelectorAll('.theme-toggle');\n    const htmlElement = document.documentElement;\n\n    function updateThemeUI(isLight) {\n        themeToggles.forEach(btn => {\n            const moon = btn.querySelector('.icon-moon');\n            const sun = btn.querySelector('.icon-sun');\n            moon.classList.toggle('hidden', isLight);\n            sun.classList.toggle('hidden', !isLight);\n        });\n    }\n\n    function applyTheme() {\n        const isLight = htmlElement.classList.toggle('light');\n        htmlElement.classList.toggle('dark', !isLight);\n        localStorage.setItem('theme', isLight ? 'light' : 'dark');\n        updateThemeUI(isLight);\n    }\n\n    function toggleTheme() {\n        \/\/ Transi\u00e7\u00e3o suave com fallback para navegadores sem suporte\n        if (!prefersReducedMotion &amp;&amp; document.startViewTransition) {\n            document.startViewTransition(() => applyTheme());\n        } else {\n            applyTheme();\n        }\n    }\n\n    themeToggles.forEach(btn => btn.addEventListener('click', toggleTheme));\n\n    \/\/ Carrega o tema salvo do usu\u00e1rio\n    const savedTheme = localStorage.getItem('theme');\n    if (savedTheme === 'light') {\n        htmlElement.classList.add('light');\n        htmlElement.classList.remove('dark');\n        updateThemeUI(true);\n    } else {\n        htmlElement.classList.add('dark');\n        updateThemeUI(false);\n    }\n\n    \/\/ --- 3. SCROLL-SPY \u2014 destaca o link da se\u00e7\u00e3o vis\u00edvel ---\n    const sections = document.querySelectorAll('section&#91;id]');\n    const sectionLinks = document.querySelectorAll('.nav-link&#91;data-section]');\n\n    if ('IntersectionObserver' in window &amp;&amp; sections.length) {\n        const spyObserver = new IntersectionObserver((entries) => {\n            entries.forEach(entry => {\n                if (entry.isIntersecting) {\n                    const id = entry.target.getAttribute('id');\n                    sectionLinks.forEach(link => {\n                        link.classList.toggle('active', link.dataset.section === id);\n                    });\n                }\n            });\n        }, { rootMargin: '-45% 0px -50% 0px', threshold: 0 });\n\n        sections.forEach(section => spyObserver.observe(section));\n    }\n\n    \/\/ --- 4. ANIMA\u00c7\u00c3O DE ENTRADA \u2014 elementos aparecem ao rolar ---\n    const revealEls = document.querySelectorAll('.reveal');\n\n    if ('IntersectionObserver' in window &amp;&amp; revealEls.length &amp;&amp; !prefersReducedMotion) {\n        const revealObserver = new IntersectionObserver((entries) => {\n            entries.forEach(entry => {\n                if (entry.isIntersecting) {\n                    entry.target.classList.add('in-view');\n                    revealObserver.unobserve(entry.target);\n                }\n            });\n        }, { threshold: 0.15 });\n\n        revealEls.forEach(el => revealObserver.observe(el));\n    } else {\n        revealEls.forEach(el => el.classList.add('in-view'));\n    }\n\n});<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Desmontando a L\u00f3gica: Como Cada Parte Funciona<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Para facilitar o entendimento, dividi o script nas quatro principais funcionalidades da aplica\u00e7\u00e3o:<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">1. Menu Mobile Limpo e Acess\u00edvel<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A fun\u00e7\u00e3o <code>toggleMenu()<\/code> manipula a classe <code>.menu-open<\/code> no nav e alterna os \u00edcones do menu hamb\u00farguer. Al\u00e9m do clique no bot\u00e3o, adicionei dois detalhes importantes de experi\u00eancia do usu\u00e1rio:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Fechamento Autom\u00e1tico:<\/strong> Ao clicar em qualquer link interno no mobile, o menu fecha sozinho para revelar o conte\u00fado.<\/li>\n\n\n\n<li><strong>Ajuste de Resize:<\/strong> Se a janela for redimensionada para uma largura de computador (<code>\u2265768px<\/code>), o estado mobile \u00e9 resetado automaticamente.<\/li>\n\n\n\n<li><strong>Acessibilidade:<\/strong> Atualiza\u00e7\u00e3o do atributo <code>aria-expanded<\/code> para leitores de tela.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">2. Dark\/Light Mode com View Transitions API<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Al\u00e9m de salvar a prefer\u00eancia do usu\u00e1rio no <code>localStorage<\/code>, usei a moderna <strong>View Transitions API<\/strong> (<code>document.startViewTransition<\/code>).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Essa tecnologia cria uma anima\u00e7\u00e3o visual de transi\u00e7\u00e3o fluida entre as trocas de cor de fundo. Quando o navegador n\u00e3o suporta a API ou o usu\u00e1rio solicitou menos anima\u00e7\u00f5es no sistema operacional, o script faz o <em>fallback<\/em> direto para a fun\u00e7\u00e3o <code>applyTheme()<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">3. Scroll-Spy com IntersectionObserver<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">O recurso de <strong>Scroll-Spy<\/strong> ativa a classe <code>.active<\/code> no menu superior indicando exatamente em qual se\u00e7\u00e3o a p\u00e1gina est\u00e1 posicionada.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Em vez de registrar eventos de <code>window.onscroll<\/code> (que consomem muito processamento), usei a API nativa <code>IntersectionObserver<\/code>. Com o par\u00e2metro <code>rootMargin: '-45% 0px -50% 0px'<\/code>, a troca de estado ocorre exatamente quando a se\u00e7\u00e3o cruza o meio da tela.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">4. Anima\u00e7\u00e3o Scroll Reveal Sem Bibliotecas Pesadas<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">As se\u00e7\u00f5es marcadas com a classe <code>.reveal<\/code> ganham a classe <code>.in-view<\/code> \u00e0 medida que entram na \u00e1rea vis\u00edvel da tela. Quando o elemento \u00e9 animado uma vez, o observador deixa de monitor\u00e1-lo com <code>revealObserver.unobserve()<\/code>, economizando mem\u00f3ria do navegador.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Tabela comparativa de APIs Web Utilit\u00e1rias<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td class=\"has-text-align-center\" data-align=\"center\"><strong>Tabela comparativa de APIs Web Utilit\u00e1rias<\/strong><\/td><td class=\"has-text-align-center\" data-align=\"center\"><strong>API \/ M\u00e9todo Utilizado<\/strong><\/td><td class=\"has-text-align-center\" data-align=\"center\"><strong>API \/ M\u00e9todo Utilizado<\/strong><\/td><\/tr><tr><td class=\"has-text-align-center\" data-align=\"center\"><strong>Troca de Tema<\/strong><\/td><td class=\"has-text-align-center\" data-align=\"center\"><strong><code>document.startViewTransition<\/code><\/strong><\/td><td class=\"has-text-align-center\" data-align=\"center\">Anima\u00e7\u00f5es suaves de layout sem usar frameworks de anima\u00e7\u00e3o pesados.<\/td><\/tr><tr><td class=\"has-text-align-center\" data-align=\"center\"><strong>Detec\u00e7\u00e3o de Scroll<\/strong><\/td><td class=\"has-text-align-center\" data-align=\"center\">IntersectionObserver<\/td><td class=\"has-text-align-center\" data-align=\"center\">Elimina eventos repetitivos de scroll, melhorando a taxa de quadros (FPS).<\/td><\/tr><tr><td class=\"has-text-align-center\" data-align=\"center\"><strong>Respeito \u00e0s Prefer\u00eancias<\/strong><\/td><td class=\"has-text-align-center\" data-align=\"center\">matchMedia(&#8216;(prefers-reduced-motion)&#8217;)<\/td><td class=\"has-text-align-center\" data-align=\"center\">Garante acessibilidade a usu\u00e1rios que sofrem com sensibilidade a anima\u00e7\u00f5es.<\/td><\/tr><tr><td class=\"has-text-align-center\" data-align=\"center\"><strong>Persist\u00eancia de Dados<\/strong><\/td><td class=\"has-text-align-center\" data-align=\"center\"><code>localStorage.getItem()<\/code><\/td><td class=\"has-text-align-center\" data-align=\"center\"><code>Mant\u00e9m o tema escolhido ativo mesmo ap\u00f3s o recarregamento do site.<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Destaques de Acessibilidade e Performance<\/h2>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\ud83d\udca1 <strong>Dica de Ouro:<\/strong> Sempre verifique a constante <code>prefersReducedMotion<\/code> antes de disparar anima\u00e7\u00f5es autom\u00e1ticas em um site. Respeitar as configura\u00e7\u00f5es do sistema operacional do usu\u00e1rio \u00e9 uma das regras mais importantes para acessibilidade web avan\u00e7ada (WCAG)<\/p>\n<\/blockquote>\n\n\n\n<h2 class=\"wp-block-heading\">Conclus\u00e3o<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Com menos de 100 linhas de JavaScript puro, consegui entregar uma interface r\u00e1pida, interativa e rica em micro-anima\u00e7\u00f5es no <strong>aqgoes.com<\/strong>. A escolha por APIs nativas como <code>IntersectionObserver<\/code> e <code>startViewTransition<\/code> garante que o portf\u00f3lio carregue instantaneamente e com nota m\u00e1xima nas ferramentas de audit como o Google Lighthouse.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Voc\u00ea j\u00e1 conhecia a View Transitions API para alternar temas ou ainda usa classes CSS tradicionais?<\/strong> Comente abaixo como costuma implementar essa funcionalidade nos seus projetos!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Intradu\u00e7\u00e3o Como Programei a L\u00f3gica JavaScript do Meu Portf\u00f3lio: Menu, Dark Mode e Anima\u00e7\u00f5es Depois de montar toda a estrutura sem\u00e2ntica em HTML5 e estilizar o projeto com Tailwind CSS no post anterior, chegou a hora de dar vida ao meu portf\u00f3lio (aqgoes.com). Uma interface moderna exige interatividade r\u00e1pida, acessibilidade e micro-intera\u00e7\u00f5es fluidas. Tudo isso [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":175,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[73,25],"tags":[71,19,61,16],"class_list":["post-174","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-front-end","category-javascript","tag-dark-mode","tag-front-end","tag-ui-ux","tag-web-development"],"_links":{"self":[{"href":"https:\/\/aqgoes.com\/blog\/wp-json\/wp\/v2\/posts\/174","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/aqgoes.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/aqgoes.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/aqgoes.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/aqgoes.com\/blog\/wp-json\/wp\/v2\/comments?post=174"}],"version-history":[{"count":1,"href":"https:\/\/aqgoes.com\/blog\/wp-json\/wp\/v2\/posts\/174\/revisions"}],"predecessor-version":[{"id":176,"href":"https:\/\/aqgoes.com\/blog\/wp-json\/wp\/v2\/posts\/174\/revisions\/176"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/aqgoes.com\/blog\/wp-json\/wp\/v2\/media\/175"}],"wp:attachment":[{"href":"https:\/\/aqgoes.com\/blog\/wp-json\/wp\/v2\/media?parent=174"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/aqgoes.com\/blog\/wp-json\/wp\/v2\/categories?post=174"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/aqgoes.com\/blog\/wp-json\/wp\/v2\/tags?post=174"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}