diff --git a/.gitignore b/.gitignore index d66aaed..8776167 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,6 @@ next-env.d.ts # Configuração local do Superset .superset/ + +# mockups do brainstorming +.superpowers/ diff --git a/CHANGELOG.md b/CHANGELOG.md index f3c4ba2..c890596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ Tudo o que muda de relevante no ESPDocs está aqui. O formato segue o ### Adicionado +- Faixa discreta de placas sugeridas nas páginas de série, na comparação e nos resultados do + seletor (`PlacasSugeridas`). Sorteia uma placa dos chips da tela, com preferência (peso 3) + pelas de loja oficial, e fica parada até o leitor usar as setas, com transição suave (respeita + "reduzir movimento"). Cada placa traz o chip, uma linha de motivo traduzida nos 6 idiomas e o + link de afiliado do idioma da página, marcado como tal. Não aparece quando o chip não tem + placa. Sem rastreamento nem cookies. - Catálogo de placas com link de afiliado por país: a planilha ganha as colunas `Link_en`, `Link_es`, `Link_fr`, `Link_de` e `Link_zh-CN`, e o botão do card usa a do idioma da página, caindo no `Link` (Brasil) quando a coluna está vazia. As colunas de link nunca viram filtro. diff --git a/app/[locale]/comparacao/page.jsx b/app/[locale]/comparacao/page.jsx index a9b6c61..1ffd29e 100644 --- a/app/[locale]/comparacao/page.jsx +++ b/app/[locale]/comparacao/page.jsx @@ -2,6 +2,7 @@ import Header from "@/components/Header"; import Footer from "@/components/Footer"; +import PlacasSugeridas from "@/components/PlacasSugeridas"; import PageHero from "@/components/PageHero"; import { useEffect, useState, useMemo, Fragment } from "react"; import { usePathname } from "next/navigation"; @@ -740,6 +741,10 @@ export default function Comparacao() { +
+ +
+

{isEs ? "Documentación oficial" : isEn ? "Official documentation" : isFr ? "Documentation officielle" : isZh ? "官方文档" : isDe ? "Offizielle Dokumentation" : "Documentação oficial"} diff --git a/app/[locale]/seletor/page.jsx b/app/[locale]/seletor/page.jsx index 0a9e036..66d6476 100644 --- a/app/[locale]/seletor/page.jsx +++ b/app/[locale]/seletor/page.jsx @@ -4,6 +4,7 @@ import { useState, useMemo } from "react"; import { usePathname } from "next/navigation"; import Header from "@/components/Header"; import Footer from "@/components/Footer"; +import PlacasSugeridas from "@/components/PlacasSugeridas"; import Link from "next/link"; import { getLocalizedHref } from "@/i18n/links"; import seriesDataPt from "@/public/series.json"; @@ -2554,6 +2555,8 @@ export default function Seletor() { + rec.key)} className="mt-10" /> + {otherRecommendations.length > 0 && (

@@ -2714,6 +2717,7 @@ export default function Seletor() {

)} + {filteredChips.length > 0 && chave)} className="mt-6" />}

)} diff --git a/app/[locale]/series/[key]/page.jsx b/app/[locale]/series/[key]/page.jsx index f8f5d9f..e93f56e 100644 --- a/app/[locale]/series/[key]/page.jsx +++ b/app/[locale]/series/[key]/page.jsx @@ -13,6 +13,7 @@ import seriesDataDe from "@/data/de/series.json"; import seriesDataZh from "@/data/zh-CN/series.json"; import ConnectionsDiagram from "@/components/ConnectionsDiagram"; import SeriesTabMenu from "@/components/SeriesTabMenu"; +import PlacasSugeridas from "@/components/PlacasSugeridas"; import PinRestrictions from "@/components/PinRestrictions"; import { getLocalizedHref } from "@/i18n/links"; import { @@ -844,6 +845,7 @@ export default async function SerieDetail({ params }) {
+
diff --git a/app/globals.css b/app/globals.css index a79106d..4dde2b7 100644 --- a/app/globals.css +++ b/app/globals.css @@ -752,6 +752,32 @@ from { opacity: 0; transform: translateY(16px); } } + /* Faixa de placas sugeridas: entra suave e desliza na direção da seta usada. + "Reduzir movimento" já zera as durações na regra global acima. */ + .sugestao-faixa { + animation: sugestao-entrada 0.6s cubic-bezier(0.16, 1, 0.3, 1) both; + } + + .sugestao-proxima { + animation: sugestao-proxima 0.45s cubic-bezier(0.16, 1, 0.3, 1) both; + } + + .sugestao-anterior { + animation: sugestao-anterior 0.45s cubic-bezier(0.16, 1, 0.3, 1) both; + } + + @keyframes sugestao-entrada { + from { opacity: 0; transform: translateY(10px); } + } + + @keyframes sugestao-proxima { + from { opacity: 0; transform: translateX(20px); } + } + + @keyframes sugestao-anterior { + from { opacity: 0; transform: translateX(-20px); } + } + .no-scrollbar { scrollbar-width: none; } diff --git a/components/PlacasSugeridas.jsx b/components/PlacasSugeridas.jsx new file mode 100644 index 0000000..f6069ca --- /dev/null +++ b/components/PlacasSugeridas.jsx @@ -0,0 +1,132 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { usePathname } from "next/navigation"; +import { ArrowUpRight, ChevronLeft, ChevronRight } from "lucide-react"; +import { boardsForChips, imageOf, reasonFor, weightedOrder } from "@/lib/boardSuggestions"; +import { linkForLocale } from "@/lib/boardLinks"; + +const TEXTOS = { + pt: { titulo: "Placa sugerida", afiliado: "Link de afiliado", ver: "Ver no AliExpress", anterior: "Placa anterior", proxima: "Próxima placa", de: "de", aba: "(abre em nova aba)", regiao: "Placas sugeridas com estes chips" }, + en: { titulo: "Suggested board", afiliado: "Affiliate link", ver: "View on AliExpress", anterior: "Previous board", proxima: "Next board", de: "of", aba: "(opens in new tab)", regiao: "Suggested boards with these chips" }, + es: { titulo: "Placa sugerida", afiliado: "Enlace de afiliado", ver: "Ver en AliExpress", anterior: "Placa anterior", proxima: "Siguiente placa", de: "de", aba: "(abre en una pestaña nueva)", regiao: "Placas sugeridas con estos chips" }, + fr: { titulo: "Carte suggérée", afiliado: "Lien d'affiliation", ver: "Voir sur AliExpress", anterior: "Carte précédente", proxima: "Carte suivante", de: "sur", aba: "(ouvre un nouvel onglet)", regiao: "Cartes suggérées avec ces puces" }, + de: { titulo: "Board-Vorschlag", afiliado: "Affiliate-Link", ver: "Auf AliExpress ansehen", anterior: "Vorheriges Board", proxima: "Nächstes Board", de: "von", aba: "(öffnet in neuem Tab)", regiao: "Board-Vorschläge mit diesen Chips" }, + "zh-CN": { titulo: "推荐开发板", afiliado: "推广链接", ver: "在 AliExpress 查看", anterior: "上一块开发板", proxima: "下一块开发板", de: "/", aba: "(在新标签页打开)", regiao: "搭载这些芯片的推荐开发板" }, +}; + +function localeFrom(pathname) { + for (const loc of ["en", "es", "fr", "de", "zh-CN"]) { + if (pathname === `/${loc}` || pathname.startsWith(`/${loc}/`)) return loc; + } + return "pt"; +} + +// Uma só busca por visita, compartilhada entre as instâncias; falha vira "sem placas". +let promessaDePlacas; +function carregarPlacas() { + if (!promessaDePlacas) { + promessaDePlacas = fetch("/api/placas") + .then((resposta) => (resposta.ok ? resposta.json() : Promise.reject(new Error(String(resposta.status))))) + .then((json) => json.data || []) + .catch(() => { + promessaDePlacas = undefined; + return []; + }); + } + return promessaDePlacas; +} + +// Faixa discreta com uma placa sorteada dos chips da página. Complementa a documentação: +// fica parada, só troca quando o leitor usa as setas, e some se não houver placa. +export default function PlacasSugeridas({ chips, className = "" }) { + const pathname = usePathname() || ""; + const locale = localeFrom(pathname); + const t = TEXTOS[locale]; + const chipsKey = [...chips].sort().join("|"); + + const [placas, setPlacas] = useState(null); + const [indice, setIndice] = useState(0); + const [sentido, setSentido] = useState("entrada"); + + useEffect(() => { + let ativo = true; + carregarPlacas().then((dados) => ativo && setPlacas(dados)); + return () => { + ativo = false; + }; + }, []); + + // O sorteio refaz só quando os dados ou o conjunto de chips mudam, não a cada render. + const ordem = useMemo(() => { + if (!placas) return null; + return weightedOrder(boardsForChips(placas, chipsKey ? chipsKey.split("|") : [], locale)); + }, [placas, chipsKey, locale]); + + useEffect(() => { + setIndice(0); + setSentido("entrada"); + }, [chipsKey]); + + // Enquanto carrega, reserva a altura da faixa para a página não pular quando ela aparece. + if (ordem === null) return