diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..acea793 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,23 @@ +--- +name: "🐛 Relato de Problema / Bug" +about: Ajude-nos a identificar e corrigir algo que não está funcionando como esperado +title: "[BUG] " +labels: ["bug"] +assignees: "" +--- + +### ⚠️ O que está acontecendo? + + +### 🔄 Como reproduzir? +1. Acesse o site em `...` +2. Clique em `...` +3. Observe o erro: `...` + +### 📱 Ambiente onde ocorreu o problema +- **Dispositivo:** (ex: Celular iPhone 14, Computador Dell) +- **Navegador:** (ex: Chrome 124, Safari, Firefox) +- **Sistema Operacional:** (ex: Android, iOS, Linux, Windows, macOS) + +### 📸 Captura de tela (opcional) + diff --git a/.github/ISSUE_TEMPLATE/content_update.md b/.github/ISSUE_TEMPLATE/content_update.md new file mode 100644 index 0000000..7a42c4f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/content_update.md @@ -0,0 +1,24 @@ +--- +name: "📝 Atualização de Conteúdo / Texto" +about: Sugira correções de texto, links, fotos ou informações do evento +title: "[DOCS] " +labels: ["documentation", "good first issue"] +assignees: "" +--- + +### 📄 Onde está o texto ou informação a ser alterada? +- [ ] No arquivo `variables.yaml` +- [ ] No arquivo `README.md` +- [ ] Em uma página Jinja2 (`pages/index.j2` ou `templates/base.j2`) +- [ ] Imagem ou foto da galeria (`static/img/`) + +### ✏️ O que deve ser alterado? +- **Texto / Conteúdo Atual:** + > ... + +- **Novo Texto / Sugestão:** + > ... + +### 🤝 Você gostaria de enviar um Pull Request com essa correção? +- [ ] Sim, quero fazer a alteração! +- [ ] Não, apenas sugerindo. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..ff5a69c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: "✨ Sugestão de Melhoria / Funcionalidade" +about: Sugira uma nova ideia, seção ou melhoria para o site da PyNE 2027 +title: "[FEAT] " +labels: ["enhancement", "good first issue"] +assignees: "" +--- + +### 💡 Qual é a ideia ou funcionalidade sugerida? + + +### 🎯 Qual problema isso resolve ou qual valor agrega à comunidade? + + +### 🎨 Proposta visual ou referências (opcional) + + +### 🤝 Você gostaria de trabalhar nessa issue? +- [ ] Sim, quero enviar um Pull Request! +- [ ] Não, apenas sugerindo para a comunidade. diff --git a/.github/labels.json b/.github/labels.json new file mode 100644 index 0000000..d1050e8 --- /dev/null +++ b/.github/labels.json @@ -0,0 +1,67 @@ +[ + { + "name": "type: feat", + "color": "0e8a16", + "description": "Nova funcionalidade ou componente visual" + }, + { + "name": "type: enhancement", + "color": "1d76db", + "description": "Melhoria em funcionalidade existente" + }, + { + "name": "type: bug", + "color": "d93f0b", + "description": "Correção de comportamento inesperado ou erro" + }, + { + "name": "type: docs", + "color": "0075ca", + "description": "Atualização de textos, README ou documentação" + }, + { + "name": "type: chore", + "color": "7057ff", + "description": "Tarefas técnicas, CI/CD ou dependências" + }, + { + "name": "type: a11y", + "color": "fbca04", + "description": "Acessibilidade (WCAG, leitores de tela, contraste)" + }, + { + "name": "scope: ui/ux", + "color": "bfdadc", + "description": "Interface, CSS, animações e responsividade" + }, + { + "name": "scope: content", + "color": "d4c5f9", + "description": "Textos, copies e informações do evento" + }, + { + "name": "scope: community", + "color": "f9d0c4", + "description": "Código de conduta, voluntários e inclusão" + }, + { + "name": "scope: sponsor", + "color": "fef2c0", + "description": "Patrocínio, cotas e media kit" + }, + { + "name": "scope: schedule", + "color": "c2e0c6", + "description": "Programação, palestras e palestrantes" + }, + { + "name": "good first issue", + "color": "7057ff", + "description": "Ideal para pessoas iniciando no código aberto" + }, + { + "name": "help wanted", + "color": "008672", + "description": "Tarefa aberta aguardando contribuição" + } +] diff --git a/.github/workflows/ci-dev.yml b/.github/workflows/ci-dev.yml new file mode 100644 index 0000000..424c8d9 --- /dev/null +++ b/.github/workflows/ci-dev.yml @@ -0,0 +1,77 @@ +name: CI - Contribuições e Deploy Staging (dev) + +on: + push: + branches: + - dev + pull_request: + branches: + - dev + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + pages: write + id-token: write + +jobs: + validate: + name: Validação de Código e Renderização + runs-on: ubuntu-latest + + steps: + - name: Checkout do código + uses: actions/checkout@v4 + + - name: Instalar uv + uses: astral-sh/setup-uv@v5 + with: + version: "latest" + enable-cache: true + + - name: Instalar Python e dependências + run: | + uv python install 3.14 + uv sync --extra dev + + - name: Verificar Lint (Ruff) + run: uv run ruff check . + + - name: Verificar Formatação (Ruff Format) + run: uv run ruff format --check . + + - name: Executar Testes Automatizados (Pytest) + run: uv run pytest -v + + - name: Testar Renderização Estática (Build) + run: uv run main render + + - name: Verificar Integridade do Site Gerado + run: | + test -f dist/index.html || (echo "dist/index.html não foi gerado!" && exit 1) + test -f dist/static/img/favicon.png || (echo "dist/static/img/favicon.png não foi encontrado!" && exit 1) + test -f dist/robots.txt || (echo "dist/robots.txt não foi gerado!" && exit 1) + test -f dist/.nojekyll || (echo "dist/.nojekyll não foi gerado!" && exit 1) + + - name: Upload de Artefato do GitHub Pages + if: github.event_name == 'push' && github.ref == 'refs/heads/dev' + uses: actions/upload-pages-artifact@v3 + with: + path: ./dist + + deploy-staging: + name: Deploy Staging no GitHub Pages + if: github.event_name == 'push' && github.ref == 'refs/heads/dev' + needs: validate + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Publicar no GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml new file mode 100644 index 0000000..ff10820 --- /dev/null +++ b/.github/workflows/ci-main.yml @@ -0,0 +1,77 @@ +name: CI/CD - Produção e Deploy (main) + +on: + push: + branches: + - main + pull_request: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + pages: write + id-token: write + +jobs: + build-and-test: + name: Testes e Build de Produção + runs-on: ubuntu-latest + + steps: + - name: Checkout do código + uses: actions/checkout@v4 + + - name: Instalar uv + uses: astral-sh/setup-uv@v5 + with: + version: "latest" + enable-cache: true + + - name: Instalar Python e dependências + run: | + uv python install 3.14 + uv sync --extra dev + + - name: Verificar Lint (Ruff) + run: uv run ruff check . + + - name: Verificar Formatação (Ruff Format) + run: uv run ruff format --check . + + - name: Executar Testes Automatizados (Pytest) + run: uv run pytest -v + + - name: Gerar Build de Produção (Render) + run: uv run main render + + - name: Verificar Integridade do Site Gerado + run: | + test -f dist/index.html || (echo "dist/index.html não foi gerado!" && exit 1) + test -f dist/static/img/favicon.png || (echo "dist/static/img/favicon.png não foi encontrado!" && exit 1) + test -f dist/robots.txt || (echo "dist/robots.txt não foi gerado!" && exit 1) + test -f dist/.nojekyll || (echo "dist/.nojekyll não foi gerado!" && exit 1) + + - name: Upload de Artefato do GitHub Pages + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: actions/upload-pages-artifact@v3 + with: + path: ./dist + + deploy: + name: Deploy para GitHub Pages + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: build-and-test + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy para GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/sync-issues.yml b/.github/workflows/sync-issues.yml new file mode 100644 index 0000000..3e56509 --- /dev/null +++ b/.github/workflows/sync-issues.yml @@ -0,0 +1,31 @@ +name: Sincronizar Issues e Labels + +on: + workflow_dispatch: + +permissions: + issues: write + +jobs: + sync: + name: Criar Labels e Issues no Repositório + runs-on: ubuntu-latest + + steps: + - name: Checkout do código + uses: actions/checkout@v4 + + - name: Instalar uv + uses: astral-sh/setup-uv@v5 + with: + version: "latest" + enable-cache: true + + - name: Instalar Python + run: uv python install 3.14 + + - name: Executar sincronização de issues e labels + run: uv run python scripts/create_issues.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} diff --git a/ISSUES.md b/ISSUES.md new file mode 100644 index 0000000..6b83921 --- /dev/null +++ b/ISSUES.md @@ -0,0 +1,198 @@ +# 📌 Quadro de Issues & Roadmap — Python Nordeste 2027 + +Quadro oficial de acompanhamento do desenvolvimento do site da **Python Nordeste 2027 (Parnaíba/PI)**. + +--- + +## 🏷️ Taxonomia de Labels Oficiais + +Para organizar as contribuições da comunidade no GitHub, utilizamos a seguinte convenção de etiquetas: + +| Categoria | Label | Cor | Descrição | +| :--- | :--- | :--- | :--- | +| **Tipo** | `type: feat` | `#0E8A16` | Nova funcionalidade ou componente visual | +| | `type: enhancement` | `#1D76DB` | Melhoria em funcionalidade existente | +| | `type: bug` | `#D93F0B` | Correção de comportamento inesperado ou erro | +| | `type: docs` | `#0075CA` | Atualização de textos, README ou documentação | +| | `type: chore` | `#7057FF` | Tarefas técnicas, CI/CD ou dependências | +| | `type: a11y` | `#FBCA04` | Acessibilidade (WCAG, leitores de tela, contraste) | +| **Escopo** | `scope: ui/ux` | `#BFDADC` | Interface, CSS, animações e responsividade | +| | `scope: content` | `#D4C5F9` | Textos, copies e informações do evento | +| | `scope: community` | `#F9D0C4` | Código de conduta, voluntários e inclusão | +| | `scope: sponsor` | `#FEF2C0` | Patrocínio, cotas e media kit | +| | `scope: schedule` | `#C2E0C6` | Programação, palestras e palestrantes | +| **Onboarding** | `good first issue` | `#7057FF` | Ideal para pessoas iniciando no código aberto | +| | `help wanted` | `#008672` | Tarefa aberta aguardando contribuição | + +--- + +## 🟢 Entregas Realizadas (Done) + +- [x] **Setup do Ambiente & Ferramentas** + - Configuração do projeto com Python 3.14 + `uv` + Hatchling. + - Estrutura de dependências de desenvolvimento (`ruff`, `pytest`, `livereload`). +- [x] **Servidor Local com LiveReload** + - Implementação do comando `uv run main start` com abertura automática de navegador e recarregamento em tempo real. + - Contribuição e correção de resiliência no repositório fork do `python-livereload`. +- [x] **Modularização Dinâmica (YAML + Jinja2)** + - Criação do [variables.yaml](variables.yaml) para controle completo de textos, datas, local e links sem alterar código. +- [x] **Identidade Visual Oficial PyNE 2027** + - Extração do brasão do mapa do Piauí/Nordeste (`static/img/pyne_emblem.png`). + - Vetorização do fundo de curvas de nível (`static/img/contour_lines.svg`). + - Aplicação da paleta *Rosa Dunas* (`#E47A9F`), *Rosa Destaque* (`#D4638A`), *Areia/Off-white* (`#F9F4E8`) e *Café Escuro* (`#2B1B22`). +- [x] **Padronização de Ícones com Font Awesome 6** + - Integração do CDN do Font Awesome 6 e substituição de emojis soltos por ícones semânticos (``, ``). +- [x] **Link do Repositório GitHub no Rodapé** + - Adicionado botão estilo badge no rodapé e pílula no Hero com link direto para o repositório da comunidade. +- [x] **Animações Visuais Modernas (GPU-Accelerated)** + - Efeito *Dunas Vivas* com deslocamento orgânico do fundo topográfico. + - Flutuação suave do brasão oficial (`floatEmblem`). + - Interação 3D tilt no emblema ao mover o cursor (`static/js/hero_animations.js`). + - Entrada escalonada dos elementos (*Staggered Reveal*). + - Pulso no pin de localização e brilho reflexivo no número "2027". + - Carrossel infinito cinematográfico com máscara de gradiente nas fotos de Parnaíba. +- [x] **Favicon com Fundo e Alto Contraste** + - Geração do favicon em formato squircle com fundo sólido Rosa Dunas, além de `static/img/apple-touch-icon.png` e `favicon.ico`. +- [x] **Copywriting Comunitário ("Pessoas > Tecnologia")** + - Ajuste da mensagem principal com o lema histórico da comunidade Python. +- [x] **Esteira de CI/CD (GitHub Actions)** + - Criação do workflow `.github/workflows/ci-dev.yml` para validação de PRs e deploy de staging no GitHub Pages. + - Criação do workflow `.github/workflows/ci-main.yml` para produção. + - Suíte de testes automatizados em `tests/test_site.py`. +- [x] **Blindagem de Staging contra Motores de Busca** + - Injeção automática de ``. + - Geração automática de `dist/robots.txt` com `Disallow: /`. + - Desativação do Google Analytics em ambiente de staging. + - Criação de `dist/.nojekyll` para compatibilidade com o GitHub Pages. + +--- + +## 🟡 Próximas Prioridades (To Do / In Progress) + +### #01 — Barra de Navegação (Navbar Fixa e Responsiva) +- **Tipo:** `type: feat` +- **Labels:** `scope: ui/ux`, `good first issue` +- **Milestone:** `v0.2.0 - Navegação & Identidade` +- **Esforço:** `Médio` +- **Descrição:** Adicionar barra de navegação no topo com logo, links de rolagem suave para as seções da página (*Início*, *Sobre*, *Destino*, *CDC*) e atalhos para as redes sociais. +- **Critérios de Aceite:** + - [ ] Navbar fixa no topo com efeito vidro/blur translúcido ao rolar a página. + - [ ] Links âncora funcionando com rolagem suave (`scroll-behavior: smooth`). + - [ ] Ícones sociais do GitHub, Instagram, LinkedIn e YouTube à direita. + - [ ] Menu hambúrguer responsivo para celulares (< 768px). + +--- + +### #02 — Seção "Sobre a Python Nordeste" +- **Tipo:** `type: feat` +- **Labels:** `scope: content`, `good first issue` +- **Milestone:** `v0.2.0 - Navegação & Identidade` +- **Esforço:** `Pequeno` +- **Descrição:** Criar um bloco institucional contextualizando a história itinerante de mais de 10 anos do evento, o compromisso com diversidade e inclusão, e a celebração da chegada inédita ao litoral do Piauí. +- **Critérios de Aceite:** + - [ ] Texto e títulos configuráveis via `variables.yaml`. + - [ ] Layout harmonizado com a paleta oficial (fundo Areia/Off-white e tipografia *Outfit*). + - [ ] Destaques em números ou marcos da comunidade (anos de história, edições passadas). + +--- + +### #03 — Página Dedicada do Código de Conduta (`/cdc.html`) +- **Tipo:** `type: feat` +- **Labels:** `scope: community`, `type: docs`, `good first issue` +- **Milestone:** `v0.2.0 - Navegação & Identidade` +- **Esforço:** `Pequeno` +- **Descrição:** Implementar uma página dedicada (`pages/cdc.j2`) com o texto completo do Código de Conduta da APyB e canais de contato da comissão de resposta a incidentes. +- **Critérios de Aceite:** + - [ ] Página acessível diretamente pela rota `/cdc.html`. + - [ ] Texto integral com formatação limpa e legível. + - [ ] Informações claras sobre como reportar incidentes de forma segura. + +--- + +### #04 — Easter Egg no DevTools Console +- **Tipo:** `type: chore` +- **Labels:** `scope: community`, `good first issue` +- **Milestone:** `v0.2.0 - Navegação & Identidade` +- **Esforço:** `Pequeno` +- **Descrição:** Injetar no console do navegador uma mensagem acolhedora com arte em texto da edição 2027, bandeiras da diversidade e o lema comunitário *Pessoas > Tecnologia*. +- **Critérios de Aceite:** + - [ ] Mensagem estilizável com CSS no `console.log`. + - [ ] Execução leve no carregamento da página sem impactar performance. + +--- + +### #05 — Rodapé Modular e Configurável (Multi-colunas) +- **Tipo:** `type: enhancement` +- **Labels:** `scope: ui/ux`, `type: feat` +- **Milestone:** `v0.2.0 - Navegação & Identidade` +- **Esforço:** `Médio` +- **Descrição:** Estruturar o rodapé de forma totalmente genérica e parametrizável via `variables.yaml`, suportando colunas opcionais (links rápidos, créditos de realização e canais de contato). +- **Critérios de Aceite:** + - [ ] Colunas opcionais: se uma seção não estiver preenchida no YAML, ela não é renderizada. + - [ ] Preservação do link oficial do repositório no GitHub com ícone Font Awesome. + - [ ] Layout responsivo em colunas no desktop e empilhado no celular. + +--- + +## 📋 Backlog de Fases Futuras (Roadmap do Evento) + +### #06 — Módulo de Voluntárias(os) & Organização +- **Tipo:** `type: feat` +- **Labels:** `scope: community`, `type: enhancement` +- **Milestone:** `v0.3.0 - Envolvimento Comunitário` +- **Esforço:** `Médio` +- **Descrição:** Suporte modular no `variables.yaml` para listar voluntárias e voluntários da organização com foto, nome, bio e links sociais, renderizados em grid dinâmico. + +--- + +### #07 — Seção de Captação de Patrocínio & Media Kit +- **Tipo:** `type: feat` +- **Labels:** `scope: sponsor`, `type: feat` +- **Milestone:** `v0.4.0 - Captação de Recursos` +- **Esforço:** `Médio` +- **Descrição:** Bloco de chamada para empresas parceiras com botões de download do Media Kit (pt-br / en) ativável condicionalmente conforme a fase de captação. + +--- + +### #08 — Grade de Patrocinadoras por Cotas +- **Tipo:** `type: feat` +- **Labels:** `scope: sponsor`, `type: feat` +- **Milestone:** `v0.4.0 - Captação de Recursos` +- **Esforço:** `Médio` +- **Descrição:** Estrutura pronta para exibir marcas apoiadoras organizadas por cotas (*Diamante, Ouro, Prata, Bronze, Apoio*), ativadas dinamicamente via YAML. + +--- + +### #09 — Seção do Local do Evento (Venue & Informações de Parnaíba) +- **Tipo:** `type: feat` +- **Labels:** `scope: content`, `scope: ui/ux` +- **Milestone:** `v0.5.0 - Experiência do Participante` +- **Esforço:** `Médio` +- **Descrição:** Bloco com detalhes do espaço em Parnaíba, dicas de deslocamento, hospedagem na região e mapa interativo integrado. + +--- + +### #10 — Página de Palestrantes e Agenda (`/speakers.html`) +- **Tipo:** `type: feat` +- **Labels:** `scope: schedule`, `type: feat` +- **Milestone:** `v0.6.0 - Grade de Programação` +- **Esforço:** `Grande` +- **Descrição:** Grid de palestrantes confirmados, keynotes e grade de horários integrada ao Pretalx ou lista estática. + +--- + +### #11 — Modal de Boas-Vindas Comunitário +- **Tipo:** `type: feat` +- **Labels:** `scope: community`, `scope: ui/ux` +- **Milestone:** `v0.5.0 - Experiência do Participante` +- **Esforço:** `Pequeno` +- **Descrição:** Popup acolhedor na primeira visita (com persistência em `localStorage`) destacando o Código de Conduta e avisos importantes. + +--- + +### #12 — Auditoria de Acessibilidade (a11y) & SEO Final +- **Tipo:** `type: a11y` +- **Labels:** `type: a11y`, `scope: ui/ux` +- **Milestone:** `v1.0.0 - Release Oficial` +- **Esforço:** `Médio` +- **Descrição:** Verificação de contraste de cores nos níveis WCAG AA/AAA, tags ARIA semânticas, navegação por teclado e pontuação 100 no Lighthouse. diff --git a/README.md b/README.md index 32720f3..f6966ce 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ O projeto utiliza o **[uv](https://docs.astral.sh/uv/)** para gerenciamento de d | --- | --- | | `uv run main start` | Compila o site e inicia o servidor local em `http://localhost:8000` | | `uv run main render` | Apenas compila as páginas e estáticos para a pasta `dist/` | +| `uv run pytest` | Executa os testes automatizados de integridade do site | | `uv run ruff check` | Analisa e valida a qualidade do código Python | | `uv run ruff format` | Formata o código Python automaticamente | @@ -87,9 +88,10 @@ Damos as boas-vindas a todas as pessoas! Se esta é a sua primeira experiência 2. **Faça suas alterações** e visualize localmente com `uv run main start`. -3. **Valide a formatação do código:** +3. **Valide a formatação e os testes locais:** ```bash uv run ruff check + uv run pytest ``` 4. **Faça o commit e envie sua branch:** @@ -99,4 +101,4 @@ Damos as boas-vindas a todas as pessoas! Se esta é a sua primeira experiência git push origin minha-contribuicao ``` -5. **Abra um Pull Request (PR)** apontando para a branch `dev`. +5. **Abra um Pull Request (PR)** apontando para a branch `dev`. A CI do GitHub executará as validações automaticamente em seu PR! diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..4fbd24c Binary files /dev/null and b/favicon.ico differ diff --git a/main.py b/main.py index 0ced965..a6922bc 100644 --- a/main.py +++ b/main.py @@ -8,20 +8,16 @@ from typer import Typer from yaml import safe_load -settings_file = Path(__file__).parent / 'variables.yaml' -pages_folder = Path(__file__).parent / 'pages' -templates_folder = Path(__file__).parent / 'templates' -static_folder = Path(__file__).parent / 'static' -output_folder = Path(__file__).parent / 'dist' +settings_file = Path(__file__).parent / "variables.yaml" +pages_folder = Path(__file__).parent / "pages" +templates_folder = Path(__file__).parent / "templates" +static_folder = Path(__file__).parent / "static" +output_folder = Path(__file__).parent / "dist" cli = Typer(no_args_is_help=True) -environment = Environment( - loader=FileSystemLoader( - searchpath=pages_folder.parent - ) -) -environment.filters['shuffle'] = lambda seq: random.sample(list(seq), k=len(seq)) +environment = Environment(loader=FileSystemLoader(searchpath=pages_folder.parent)) +environment.filters["shuffle"] = lambda seq: random.sample(list(seq), k=len(seq)) def load_settings() -> dict: @@ -41,13 +37,32 @@ def render(): output_folder.mkdir(exist_ok=True) static_folder.copy(output_folder / static_folder.name) - for file in pages_folder.glob('*.j2'): + root_favicon = Path(__file__).parent / "favicon.ico" + if root_favicon.exists(): + shutil.copy(root_favicon, output_folder / "favicon.ico") + + for file in pages_folder.glob("*.j2"): template_path = file.relative_to(pages_folder.parent).as_posix() template = environment.get_template(template_path) rendered_content = template.render(settings) - output_file = output_folder / file.with_suffix('.html').name + output_file = output_folder / file.with_suffix(".html").name output_file.write_text(rendered_content) + # Geração automática de robots.txt com base no ambiente (staging vs production) + is_staging = settings.get("environment") == "staging" or not settings.get( + "allow_indexing", True + ) + robots_file = output_folder / "robots.txt" + if is_staging: + robots_file.write_text("User-agent: *\nDisallow: /\n") + else: + site_url = settings.get("event", {}).get("url", "").rstrip("/") + sitemap_line = f"\nSitemap: {site_url}/sitemap.xml" if site_url else "" + robots_file.write_text(f"User-agent: *\nAllow: /{sitemap_line}\n") + + # Arquivo .nojekyll para o GitHub Pages servir arquivos estáticos diretamente + (output_folder / ".nojekyll").touch() + @cli.command() def start(port: int = 8000, host: str = "127.0.0.1"): @@ -60,17 +75,25 @@ def start(port: int = 8000, host: str = "127.0.0.1"): server = Server() # Monitora dados, templates, páginas e estáticos server.watch(str(settings_file), render) - server.watch(str(pages_folder / '*.j2'), render) - server.watch(str(templates_folder / '*.j2'), render) + server.watch(str(pages_folder / "*.j2"), render) + server.watch(str(templates_folder / "*.j2"), render) server.watch(str(static_folder), render) - print(f"🔥 Servidor com Live Reload rodando em http://{host}:{port}/ (Ctrl+C para encerrar)") + print( + f"🔥 Servidor com Live Reload rodando em http://{host}:{port}/ (Ctrl+C para encerrar)" + ) server.serve(root=str(output_folder), host=host, port=port, open_url=True) except ImportError: - handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=str(output_folder)) + handler = functools.partial( + http.server.SimpleHTTPRequestHandler, directory=str(output_folder) + ) server = http.server.ThreadingHTTPServer((host, port), handler) - print(f"Servidor rodando em http://{host}:{port}/ (pressione Ctrl+C para encerrar)") - print("💡 Dica: instale as dependências de dev ('uv sync --extra dev') para ativar o Live Reload automático.") + print( + f"Servidor rodando em http://{host}:{port}/ (pressione Ctrl+C para encerrar)" + ) + print( + "💡 Dica: instale as dependências de dev ('uv sync --extra dev') para ativar o Live Reload automático." + ) try: server.serve_forever() except KeyboardInterrupt: diff --git a/pages/index.j2 b/pages/index.j2 index 6ee1327..171e514 100644 --- a/pages/index.j2 +++ b/pages/index.j2 @@ -5,45 +5,116 @@ {% endblock %} {% block content %} - +
+ +
+ + {% endblock %} {% block extra_js %} + {% endblock %} \ No newline at end of file diff --git a/scripts/create_issues.py b/scripts/create_issues.py new file mode 100755 index 0000000..14869c8 --- /dev/null +++ b/scripts/create_issues.py @@ -0,0 +1,344 @@ +#!/usr/bin/env python3 +""" +Script de automação para criar Labels e Issues no GitHub. +Pode ser executado localmente (com GITHUB_TOKEN) ou via GitHub Actions (workflow_dispatch). +""" + +import json +import os +import shutil +import subprocess +import sys +import urllib.error +import urllib.parse +import urllib.request +from pathlib import Path + +REPO = os.environ.get("GITHUB_REPOSITORY", "pythonNordeste/pyne2027") +TOKEN = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") + +if not TOKEN and shutil.which("gh"): + try: + proc = subprocess.run( + ["gh", "auth", "token"], capture_output=True, text=True, check=True + ) + TOKEN = proc.stdout.strip() + except subprocess.CalledProcessError, FileNotFoundError, OSError: + TOKEN = None + +if not TOKEN: + print("❌ Erro: GITHUB_TOKEN ou GH_TOKEN não encontrado nas variáveis de ambiente.") + print("💡 Para executar localmente, defina: export GITHUB_TOKEN='seu_token'") + print( + "💡 Ou execute via GitHub Actions na aba 'Actions > Sincronizar Issues e Labels > Run workflow'." + ) + sys.exit(1) + +HEADERS = { + "Authorization": f"Bearer {TOKEN}", + "Accept": "application/vnd.github+json", + "User-Agent": "pyne2027-issues-syncer", + "X-GitHub-Api-Version": "2022-11-28", +} + + +def api_request(endpoint: str, method: str = "GET", data: dict | None = None): + url = f"https://api.github.com/{endpoint.lstrip('/')}" + body = json.dumps(data).encode("utf-8") if data else None + req = urllib.request.Request(url, data=body, headers=HEADERS, method=method) + try: + with urllib.request.urlopen(req) as resp: + content = resp.read().decode("utf-8") + return json.loads(content) if content else {} + except urllib.error.HTTPError as e: + error_body = e.read().decode("utf-8") + return {"error": e.code, "message": error_body} + + +def sync_labels(): + print(f"📦 Sincronizando labels em {REPO}...") + labels_file = Path(__file__).parent.parent / ".github" / "labels.json" + if not labels_file.exists(): + print("⚠️ Arquivo .github/labels.json não encontrado.") + return + + labels = json.loads(labels_file.read_text(encoding="utf-8")) + + # Buscar labels existentes + existing = api_request(f"repos/{REPO}/labels?per_page=100") + if isinstance(existing, dict) and "error" in existing: + print(f"❌ Erro ao listar labels: {existing.get('message')}") + if existing.get("error") == 404: + print( + "💡 Dica: Erro 404 indica que o token atual não tem acesso ao repositório." + ) + print( + " Se estiver usando gh CLI, reautentique com: gh auth login -w -s repo" + ) + print( + " Ou defina GITHUB_TOKEN com um Personal Access Token (Classic) com escopo 'repo'." + ) + return + + existing_names = {l["name"].lower(): l["name"] for l in existing} + + for label in labels: + name = label["name"] + color = label["color"].lstrip("#") + desc = label.get("description", "") + + if name.lower() in existing_names: + real_name = existing_names[name.lower()] + endpoint = f"repos/{REPO}/labels/{urllib.parse.quote(real_name)}" + api_request( + endpoint, method="PATCH", data={"color": color, "description": desc} + ) + print(f" 🔄 Label atualizada: {name}") + else: + endpoint = f"repos/{REPO}/labels" + api_request( + endpoint, + method="POST", + data={"name": name, "color": color, "description": desc}, + ) + print(f" ✨ Label criada: {name}") + + +ISSUES = [ + { + "title": "feat: barra de navegação superior fixa e responsiva", + "labels": ["type: feat", "scope: ui/ux", "good first issue"], + "body": """### 💡 Descrição da Funcionalidade +Implementar barra de navegação no topo com logo, links de rolagem suave para as seções da página (*Início*, *Sobre*, *Destino*, *CDC*) e atalhos para as redes sociais. + +### 🎯 Critérios de Aceite +- [ ] Navbar fixa no topo com efeito vidro/blur translúcido ao rolar a página. +- [ ] Links âncora funcionando com rolagem suave (`scroll-behavior: smooth`). +- [ ] Ícones sociais do GitHub, Instagram, LinkedIn e YouTube à direita. +- [ ] Menu hambúrguer responsivo para celulares (< 768px). + +### 🏷️ Informações Adicionais +- **Milestone:** v0.2.0 - Navegação & Identidade +- **Esforço estimado:** Médio +""", + }, + { + "title": "feat: seção 'Sobre a Python Nordeste' contando história e valores", + "labels": ["type: feat", "scope: content", "good first issue"], + "body": """### 💡 Descrição da Funcionalidade +Criar um bloco institucional contextualizando a história itinerante de mais de 10 anos do evento, o compromisso com diversidade e inclusão, e a celebração da chegada inédita ao litoral do Piauí. + +### 🎯 Critérios de Aceite +- [ ] Texto e títulos configuráveis via `variables.yaml`. +- [ ] Layout harmonizado com a paleta oficial (fundo Areia/Off-white e tipografia *Outfit*). +- [ ] Destaques em números ou marcos da comunidade (anos de história, edições passadas). + +### 🏷️ Informações Adicionais +- **Milestone:** v0.2.0 - Navegação & Identidade +- **Esforço estimado:** Pequeno +""", + }, + { + "title": "feat: página dedicada do Código de Conduta (/cdc.html)", + "labels": ["type: feat", "scope: community", "type: docs", "good first issue"], + "body": """### 💡 Descrição da Funcionalidade +Implementar uma página dedicada (`pages/cdc.j2`) com o texto completo do Código de Conduta da APyB e canais de contato da comissão de resposta a incidentes. + +### 🎯 Critérios de Aceite +- [ ] Página acessível diretamente pela rota `/cdc.html`. +- [ ] Texto integral com formatação limpa e legível. +- [ ] Informações claras sobre como reportar incidentes de forma segura. + +### 🏷️ Informações Adicionais +- **Milestone:** v0.2.0 - Navegação & Identidade +- **Esforço estimado:** Pequeno +""", + }, + { + "title": "chore: easter egg comunitário no console do DevTools", + "labels": ["type: chore", "scope: community", "good first issue"], + "body": """### 💡 Descrição da Funcionalidade +Injetar no console do navegador uma mensagem acolhedora com arte em texto da edição 2027, bandeiras da diversidade e o lema comunitário *Pessoas > Tecnologia*. + +### 🎯 Critérios de Aceite +- [ ] Mensagem estilizável com CSS no `console.log`. +- [ ] Execução leve no carregamento da página sem impactar performance. + +### 🏷️ Informações Adicionais +- **Milestone:** v0.2.0 - Navegação & Identidade +- **Esforço estimado:** Pequeno +""", + }, + { + "title": "feat: rodapé modular e configurável em multi-colunas via variables.yaml", + "labels": ["type: feat", "type: enhancement", "scope: ui/ux"], + "body": """### 💡 Descrição da Funcionalidade +Estruturar o rodapé de forma totalmente genérica e parametrizável via `variables.yaml`, suportando colunas opcionais (links rápidos, créditos de realização e canais de contato). + +### 🎯 Critérios de Aceite +- [ ] Colunas opcionais: se uma seção não estiver preenchida no YAML, ela não é renderizada. +- [ ] Preservação do link oficial do repositório no GitHub com ícone Font Awesome. +- [ ] Layout responsivo em colunas no desktop e empilhado no celular. + +### 🏷️ Informações Adicionais +- **Milestone:** v0.2.0 - Navegação & Identidade +- **Esforço estimado:** Médio +""", + }, + { + "title": "feat: módulo de voluntárias(os) e equipe organizadora", + "labels": ["type: feat", "type: enhancement", "scope: community"], + "body": """### 💡 Descrição da Funcionalidade +Suporte modular no `variables.yaml` para listar voluntárias e voluntários da organização com foto, nome, bio e links sociais, renderizados em grid dinâmico. + +### 🎯 Critérios de Aceite +- [ ] Lista em YAML ou JSON com fallback para fotos padrão. +- [ ] Grid responsivo com cards e links sociais. + +### 🏷️ Informações Adicionais +- **Milestone:** v0.3.0 - Envolvimento Comunitário +- **Esforço estimado:** Médio +""", + }, + { + "title": "feat: seção de captação de patrocínio com download de Media Kit", + "labels": ["type: feat", "scope: sponsor"], + "body": """### 💡 Descrição da Funcionalidade +Bloco de chamada para empresas parceiras com botões de download do Media Kit (pt-br / en) ativável condicionalmente conforme a fase de captação. + +### 🎯 Critérios de Aceite +- [ ] Download do Media Kit em PDF condicional. +- [ ] E-mail de contato para propostas e cotas. + +### 🏷️ Informações Adicionais +- **Milestone:** v0.4.0 - Captação de Recursos +- **Esforço estimado:** Médio +""", + }, + { + "title": "feat: grade de patrocinadoras organizadas por cotas", + "labels": ["type: feat", "scope: sponsor"], + "body": """### 💡 Descrição da Funcionalidade +Estrutura pronta para exibir marcas apoiadoras organizadas por cotas (*Diamante, Ouro, Prata, Bronze, Apoio*), ativadas dinamicamente via YAML. + +### 🎯 Critérios de Aceite +- [ ] Logos organizadas por nível de patrocínio com links externos. +- [ ] Espaço dedicado para parceiros institucionais e apoios locais. + +### 🏷️ Informações Adicionais +- **Milestone:** v0.4.0 - Captação de Recursos +- **Esforço estimado:** Médio +""", + }, + { + "title": "feat: seção do local do evento (venue) e mapa interativo", + "labels": ["type: feat", "scope: content", "scope: ui/ux"], + "body": """### 💡 Descrição da Funcionalidade +Bloco com detalhes do espaço em Parnaíba, dicas de deslocamento, hospedagem na região e mapa interativo integrado. + +### 🎯 Critérios de Aceite +- [ ] Informações de endereço e pontos de referência. +- [ ] Mapa integrado ou link direto para Google Maps / OpenStreetMap. +- [ ] Dicas de transporte e aeroportos próximos (PHB, THE, FOR). + +### 🏷️ Informações Adicionais +- **Milestone:** v0.5.0 - Experiência do Participante +- **Esforço estimado:** Médio +""", + }, + { + "title": "feat: página de palestrantes e agenda (/speakers.html)", + "labels": ["type: feat", "scope: schedule"], + "body": """### 💡 Descrição da Funcionalidade +Grid de palestrantes confirmados, keynotes e grade de horários integrada ao Pretalx ou lista estática. + +### 🎯 Critérios de Aceite +- [ ] Página `/speakers.html` ou `/agenda.html`. +- [ ] Visualização por dia e por trilha. +- [ ] Destaque para palestrantes convidadas(os). + +### 🏷️ Informações Adicionais +- **Milestone:** v0.6.0 - Grade de Programação +- **Esforço estimado:** Grande +""", + }, + { + "title": "feat: modal de boas-vindas comunitário no primeiro acesso", + "labels": ["type: feat", "scope: community", "scope: ui/ux"], + "body": """### 💡 Descrição da Funcionalidade +Popup acolhedor na primeira visita (com persistência em `localStorage`) destacando o Código de Conduta e avisos importantes. + +### 🎯 Critérios de Aceite +- [ ] Exibição apenas na primeira visita via `localStorage`. +- [ ] Opção fácil de fechar (botão X ou clique fora). +- [ ] Respeito a acessibilidade de teclado (ESC fecha o modal). + +### 🏷️ Informações Adicionais +- **Milestone:** v0.5.0 - Experiência do Participante +- **Esforço estimado:** Pequeno +""", + }, + { + "title": "a11y: auditoria de acessibilidade, contraste e pontuação 100 no Lighthouse", + "labels": ["type: a11y", "scope: ui/ux"], + "body": """### 💡 Descrição da Funcionalidade +Verificação de contraste de cores nos níveis WCAG AA/AAA, tags ARIA semânticas, navegação por teclado e pontuação 100 no Lighthouse. + +### 🎯 Critérios de Aceite +- [ ] Contraste verificado para textos e fundos. +- [ ] Navegação 100% acessível por teclado (foco visível). +- [ ] Alt tags descritivas em todas as imagens. +- [ ] Pontuação mínima de 95+ em Acessibilidade e Boas Práticas no Google Lighthouse. + +### 🏷️ Informações Adicionais +- **Milestone:** v1.0.0 - Release Oficial +- **Esforço estimado:** Médio +""", + }, +] + + +def sync_issues(): + print(f"\n📋 Verificando issues existentes em {REPO}...") + existing = api_request(f"repos/{REPO}/issues?state=all&per_page=100") + if isinstance(existing, dict) and "error" in existing: + print(f"❌ Erro ao listar issues: {existing.get('message')}") + if existing.get("error") == 404: + print( + "💡 Dica: Erro 404 indica que o token atual não tem acesso ao repositório." + ) + print( + " Se estiver usando gh CLI, reautentique com: gh auth login -w -s repo" + ) + print( + " Ou defina GITHUB_TOKEN com um Personal Access Token (Classic) com escopo 'repo'." + ) + return + + existing_titles = { + i["title"].strip().lower() for i in existing if "pull_request" not in i + } + + for issue in ISSUES: + title = issue["title"] + if title.strip().lower() in existing_titles: + print(f" ⏭️ Já existe: {title}") + continue + + data = { + "title": title, + "body": issue["body"], + "labels": issue["labels"], + } + res = api_request(f"repos/{REPO}/issues", method="POST", data=data) + if isinstance(res, dict) and "error" in res: + print(f" ❌ Erro ao criar '{title}': {res.get('message')}") + else: + issue_num = res.get("number", "?") + print(f" ✅ Criada #{issue_num}: {title}") + + +if __name__ == "__main__": + sync_labels() + sync_issues() + print("\n🎉 Processo concluído com sucesso!") diff --git a/static/css/main.css b/static/css/main.css index 55c3a6c..f87f2e2 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -1,28 +1,541 @@ -body { +/* ========================================================================== + Python Nordeste 2027 (PyNE 2027) - Estilos Principais + Identidade Visual: Rosa Dunas, Areia Quente e Linhas Topográficas + ========================================================================== */ + +*, *::before, *::after { + box-sizing: border-box; margin: 0; + padding: 0; } -header { +html { + scroll-behavior: smooth; +} + +body { + font-family: var(--font-body); + color: var(--text-dark); + background-color: var(--accent-color); + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + +/* ========================================================================== + Hero Section + ========================================================================== */ + +.hero-section { + position: relative; + background: linear-gradient(145deg, var(--primary-color) 0%, var(--secondary-color) 100%); + color: #FFFFFF; + padding: 3.5rem 1.5rem 4.5rem; text-align: center; + overflow: hidden; + box-shadow: 0 10px 30px -10px rgba(159, 53, 92, 0.4); } -.header-text h1 { - font-size: 4rem; - margin: 0; +/* Vento suave nas curvas de nível das dunas */ +.hero-section::before { + content: ""; + position: absolute; + inset: -35px; + background: url("../img/contour_lines.svg") center/cover no-repeat; + opacity: 0.85; + pointer-events: none; + z-index: 1; + animation: duneWind 26s ease-in-out infinite alternate; + will-change: transform; +} + +@keyframes duneWind { + 0% { + transform: scale(1) translate(0, 0); + } + 50% { + transform: scale(1.04) translate(-14px, -8px); + } + 100% { + transform: scale(1) translate(12px, 6px); + } +} + +.hero-container { + position: relative; + max-width: 900px; + margin: 0 auto; + z-index: 2; +} + +/* Badge de Localização e Ano */ +.location-badge { + display: inline-flex; + align-items: center; + gap: 0.5rem; + background: rgba(249, 244, 232, 0.22); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.4); + padding: 0.45rem 1.25rem; + border-radius: 9999px; + font-size: 0.95rem; + font-weight: 600; + color: #FFFFFF; + margin-bottom: 2rem; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); + transition: transform 0.3s ease, background 0.3s ease; +} + +.location-badge:hover { + transform: translateY(-2px); + background: rgba(249, 244, 232, 0.32); +} + +.badge-pin { + font-size: 1.1rem; + display: inline-block; + color: #FFFFFF; + animation: pinPulse 2.8s ease-in-out infinite; + transform-origin: bottom center; +} + +@keyframes pinPulse { + 0%, 100% { + transform: scale(1) translateY(0); + } + 14% { + transform: scale(1.28) translateY(-3px); + color: #FFE600; + } + 28% { + transform: scale(1) translateY(0); + } +} + +/* Logo Oficial PyNE 2027 */ +.logo-wrapper { + max-width: 380px; + margin: 0 auto 2.2rem; + position: relative; + perspective: 1000px; +} + +.logo-tilt-container { + transform-style: preserve-3d; + transition: transform 0.15s ease-out; + will-change: transform; +} + +.hero-logo { + width: 100%; + height: auto; + display: block; + background: transparent; + filter: drop-shadow(0 18px 35px rgba(60, 20, 35, 0.3)); + animation: floatEmblem 6s ease-in-out infinite; + transition: filter 0.4s ease; + will-change: transform, filter; +} + +@keyframes floatEmblem { + 0%, 100% { + transform: translateY(0); + filter: drop-shadow(0 18px 35px rgba(60, 20, 35, 0.3)); + } + 50% { + transform: translateY(-9px); + filter: drop-shadow(0 30px 48px rgba(60, 20, 35, 0.4)); + } +} + +.hero-logo:hover { + filter: drop-shadow(0 34px 54px rgba(60, 20, 35, 0.48)); +} + +/* Tipografia do Hero */ +.hero-title { + font-family: var(--font-display); + font-size: clamp(2.2rem, 5.5vw, 3.6rem); + font-weight: 800; + letter-spacing: -0.02em; + margin-bottom: 0.75rem; + color: #FFFFFF; + text-shadow: 0 3px 12px rgba(0, 0, 0, 0.15); +} + +.edition-highlight { + display: inline-block; + color: var(--accent-color); + background: linear-gradient(110deg, var(--accent-color) 20%, #FFFFFF 48%, var(--accent-color) 76%); + background-size: 200% 100%; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + animation: shimmer2027 6s ease-in-out infinite; +} + +@keyframes shimmer2027 { + 0% { + background-position: 150% 0; + } + 45%, 100% { + background-position: -50% 0; + } +} + +.hero-tagline { + font-size: clamp(1.15rem, 2.5vw, 1.4rem); + font-weight: 600; + color: var(--accent-color); + margin-bottom: 1rem; + line-height: 1.4; +} + +.hero-description { + font-size: clamp(0.98rem, 1.8vw, 1.1rem); + line-height: 1.65; + max-width: 660px; + margin: 0 auto 2rem; + color: rgba(255, 255, 255, 0.95); +} + +/* Botões de Ação */ +.action-buttons { + display: flex; + flex-wrap: wrap; + gap: 1rem; + justify-content: center; + margin-bottom: 2.5rem; +} + +.btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.85rem 1.85rem; + border-radius: 9999px; + font-weight: 700; + font-size: 1rem; + text-decoration: none; + transition: all 0.25s ease; + cursor: pointer; +} + +.btn-primary { + background: var(--accent-color); + color: var(--secondary-color); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15); +} + +.btn-primary:hover { + transform: translateY(-3px); + box-shadow: 0 12px 25px rgba(0, 0, 0, 0.25); + background: #FFFFFF; +} + +.btn-secondary { + background: transparent; + color: var(--accent-color); + border: 2px solid var(--accent-color); +} + +.btn-secondary:hover { + background: rgba(249, 244, 232, 0.15); + transform: translateY(-3px); +} + +/* Redes Sociais */ +.social-section { + margin-top: 1.5rem; +} + +.social-call { + font-size: 0.92rem; + font-weight: 600; + color: rgba(255, 255, 255, 0.85); + margin-bottom: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.social-badges { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + justify-content: center; +} + +.social-pill { + display: inline-flex; + align-items: center; + gap: 0.45rem; + background: rgba(255, 255, 255, 0.92); + color: var(--secondary-color); + padding: 0.55rem 1.25rem; + border-radius: 9999px; + font-weight: 700; + font-size: 0.92rem; + text-decoration: none; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + transition: all 0.25s ease; +} + +.social-pill i { + font-size: 1.05rem; + transition: transform 0.2s ease, color 0.2s ease; +} + +.social-pill:hover { + transform: translateY(-3px); + background: #FFFFFF; + color: var(--text-dark); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.16); +} + +.social-pill:hover i { + transform: scale(1.15); +} + +.social-pill.instagram:hover i { + color: #E1306C; +} + +.social-pill.linkedin:hover i { + color: #0A66C2; } -footer { +.social-pill.youtube:hover i { + color: #FF0000; +} + +.social-pill.telegram:hover i { + color: #24A1DE; +} + +.social-pill.github:hover i { + color: #24292E; +} + +.footer-heart { + color: var(--primary-color); + margin: 0 0.25rem; +} + + +/* ========================================================================== + Destination Section (Galeria de Parnaíba) + ========================================================================== */ + +.destination-section { + padding: 5rem 1.5rem 4rem; + background: var(--accent-color); text-align: center; - padding: 2rem 1rem; - font-size: 0.9rem; - color: #666; } -footer a { - color: inherit; +.section-header { + max-width: 680px; + margin: 0 auto 2.5rem; +} + +.section-tag { + display: inline-block; + color: var(--secondary-color); + font-weight: 800; + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.1em; + margin-bottom: 0.5rem; +} + +.section-title { + font-family: var(--font-display); + font-size: clamp(1.9rem, 4vw, 2.75rem); + font-weight: 800; + color: var(--text-dark); + letter-spacing: -0.02em; + margin-bottom: 0.85rem; +} + +.section-subtitle { + font-size: 1.05rem; + line-height: 1.6; + color: #55444D; +} + +.phb-photos-carousel img { + border-radius: 14px; + box-shadow: 0 8px 24px rgba(43, 27, 34, 0.12); + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.phb-photos-carousel img:hover { + transform: scale(1.03); + box-shadow: 0 12px 28px rgba(43, 27, 34, 0.2); +} + +/* ========================================================================== + Footer + ========================================================================== */ + +#footer { + background: var(--text-dark); + color: rgba(249, 244, 232, 0.85); + padding: 3.5rem 1.5rem 3rem; + text-align: center; + font-size: 0.95rem; +} + +.footer-content { + max-width: 700px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; + gap: 1.1rem; +} + +.footer-cdc { + margin: 0; + line-height: 1.6; +} + +#footer a { + color: var(--accent-color); text-decoration: underline; + text-underline-offset: 4px; + font-weight: 600; + transition: color 0.2s ease; +} + +#footer a:hover { + color: #FFFFFF; +} + +.footer-repo { + margin: 0.25rem 0; +} + +.footer-github-link { + display: inline-flex; + align-items: center; + gap: 0.6rem; + padding: 0.55rem 1.3rem; + background: rgba(249, 244, 232, 0.08); + border: 1px solid rgba(249, 244, 232, 0.22); + border-radius: 9999px; + color: var(--accent-color) !important; + text-decoration: none !important; + font-size: 0.9rem; + font-weight: 600; + letter-spacing: 0.02em; + transition: all 0.25s ease; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.footer-github-link i { + font-size: 1.2rem; + transition: transform 0.2s ease; +} + +.footer-github-link:hover { + background: rgba(249, 244, 232, 0.18); + border-color: rgba(249, 244, 232, 0.45); + color: #FFFFFF !important; + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3); +} + +.footer-github-link:hover i { + transform: scale(1.15); } -footer a:hover { - color: #000; +.footer-sub { + margin: 0; + font-size: 0.85rem; + color: rgba(249, 244, 232, 0.55); +} + +/* ========================================================================== + Responsividade + ========================================================================== */ + +@media (max-width: 600px) { + .hero-section { + padding: 2.5rem 1rem 3.5rem; + } + + .location-badge { + font-size: 0.85rem; + padding: 0.35rem 1rem; + } + + .social-badges { + gap: 0.5rem; + } + + .social-pill { + padding: 0.45rem 1rem; + font-size: 0.85rem; + } + + .destination-section { + padding: 3.5rem 1rem 2.5rem; + } } + +/* ========================================================================== + Entradas Escalonadas (Staggered Reveal) + ========================================================================== */ + +@keyframes heroReveal { + from { + opacity: 0; + transform: translateY(22px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.hero-animate-1 { + animation: heroReveal 0.75s cubic-bezier(0.16, 1, 0.3, 1) 0.05s both; +} + +.hero-animate-2 { + animation: heroReveal 0.85s cubic-bezier(0.16, 1, 0.3, 1) 0.2s both; +} + +.hero-animate-3 { + animation: heroReveal 0.85s cubic-bezier(0.16, 1, 0.3, 1) 0.35s both; +} + +.hero-animate-4 { + animation: heroReveal 0.85s cubic-bezier(0.16, 1, 0.3, 1) 0.5s both; +} + +.hero-animate-5 { + animation: heroReveal 0.85s cubic-bezier(0.16, 1, 0.3, 1) 0.65s both; +} + +/* Acessibilidade: Desativa animações se o usuário preferir menos movimento */ +@media (prefers-reduced-motion: reduce) { + .hero-section::before, + .badge-pin, + .hero-logo, + .edition-highlight, + .hero-animate-1, + .hero-animate-2, + .hero-animate-3, + .hero-animate-4, + .hero-animate-5 { + animation: none !important; + transform: none !important; + opacity: 1 !important; + } + + .edition-highlight { + background: none !important; + -webkit-text-fill-color: var(--accent-color) !important; + } +} + diff --git a/static/css/phb_photos_animation.css b/static/css/phb_photos_animation.css index 6fd7f73..c4a51da 100644 --- a/static/css/phb_photos_animation.css +++ b/static/css/phb_photos_animation.css @@ -1,12 +1,17 @@ .phb-photos { overflow: hidden; width: 100%; + position: relative; + /* Suavização cinematográfica nas bordas laterais */ + mask-image: linear-gradient(to right, transparent 0%, black 8%, black 92%, transparent 100%); + -webkit-mask-image: linear-gradient(to right, transparent 0%, black 8%, black 92%, transparent 100%); } .phb-photos-carousel { display: flex; width: max-content; - animation: phb-photos-scroll 50s linear infinite; + animation: phb-photos-scroll 60s linear infinite; + will-change: transform; } .phb-photos:hover .phb-photos-carousel { @@ -14,13 +19,19 @@ } .phb-photos-carousel img { - height: 300px; + height: 290px; width: auto; flex-shrink: 0; object-fit: cover; - margin-right: 10px; - margin-top: 10px; - margin-bottom: 10px; + margin: 12px 8px; + border-radius: 16px; + box-shadow: 0 8px 24px rgba(43, 27, 34, 0.12); + transition: transform 0.35s cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 0.35s ease; +} + +.phb-photos-carousel img:hover { + transform: scale(1.05) translateY(-4px); + box-shadow: 0 16px 32px rgba(43, 27, 34, 0.24); } @keyframes phb-photos-scroll { @@ -36,4 +47,9 @@ .phb-photos-carousel { animation: none; } + .phb-photos { + mask-image: none; + -webkit-mask-image: none; + overflow-x: auto; + } } \ No newline at end of file diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..4fbd24c Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/img/apple-touch-icon.png b/static/img/apple-touch-icon.png new file mode 100644 index 0000000..5696d6d Binary files /dev/null and b/static/img/apple-touch-icon.png differ diff --git a/static/img/contour_lines.svg b/static/img/contour_lines.svg new file mode 100644 index 0000000..3f2cc56 --- /dev/null +++ b/static/img/contour_lines.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/static/img/favicon.png b/static/img/favicon.png new file mode 100644 index 0000000..b1ad4fc Binary files /dev/null and b/static/img/favicon.png differ diff --git a/static/img/logo_pyne2027.png b/static/img/logo_pyne2027.png new file mode 100644 index 0000000..6613b9c Binary files /dev/null and b/static/img/logo_pyne2027.png differ diff --git a/static/img/pyne_emblem.png b/static/img/pyne_emblem.png new file mode 100644 index 0000000..883ad33 Binary files /dev/null and b/static/img/pyne_emblem.png differ diff --git a/static/js/hero_animations.js b/static/js/hero_animations.js new file mode 100644 index 0000000..2858481 --- /dev/null +++ b/static/js/hero_animations.js @@ -0,0 +1,61 @@ +/** + * Animações interativas do Hero da PyNE 2027 + * - Efeito 3D Tilt suave no emblema ao mover o mouse + * - Respeito automático a prefers-reduced-motion + */ +document.addEventListener('DOMContentLoaded', () => { + const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + if (prefersReducedMotion) return; + + const hero = document.getElementById('hero'); + const tiltContainer = document.getElementById('logo-tilt'); + + if (!hero || !tiltContainer || !window.matchMedia('(pointer: fine)').matches) { + return; + } + + let rafId = null; + let targetRotateX = 0; + let targetRotateY = 0; + let currentRotateX = 0; + let currentRotateY = 0; + + function updateTilt() { + // Interpolação suave (lerp) + currentRotateX += (targetRotateX - currentRotateX) * 0.1; + currentRotateY += (targetRotateY - currentRotateY) * 0.1; + + tiltContainer.style.transform = `perspective(900px) rotateX(${currentRotateX.toFixed(2)}deg) rotateY(${currentRotateY.toFixed(2)}deg)`; + + if (Math.abs(targetRotateX - currentRotateX) > 0.05 || Math.abs(targetRotateY - currentRotateY) > 0.05) { + rafId = requestAnimationFrame(updateTilt); + } else { + rafId = null; + } + } + + hero.addEventListener('mousemove', (e) => { + const rect = tiltContainer.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + + const maxAngle = 10; + const deltaX = (e.clientX - centerX) / (window.innerWidth / 2); + const deltaY = (e.clientY - centerY) / (window.innerHeight / 2); + + targetRotateY = Math.max(-maxAngle, Math.min(maxAngle, deltaX * maxAngle)); + targetRotateX = Math.max(-maxAngle, Math.min(maxAngle, -deltaY * maxAngle)); + + if (!rafId) { + rafId = requestAnimationFrame(updateTilt); + } + }); + + hero.addEventListener('mouseleave', () => { + targetRotateX = 0; + targetRotateY = 0; + if (!rafId) { + rafId = requestAnimationFrame(updateTilt); + } + }); +}); diff --git a/static/js/phb_photos_animation.js b/static/js/phb_photos_animation.js index 1a72e8c..9436fa0 100644 --- a/static/js/phb_photos_animation.js +++ b/static/js/phb_photos_animation.js @@ -9,16 +9,17 @@ function setBackgroundImage(index) { } document.addEventListener('DOMContentLoaded', function() { - let images = Array.from( - { length: IMAGES_COUNT }, - (_, i) => i - ); - const container = document.querySelector('.phb-photos-carousel'); + if (!container) return; - for (let index of images) { - const img = document.createElement('img'); - img.src = getImageUrl(index); - container.appendChild(img); + // Renderiza duas sequências para loop infinito contínuo (0 a -50%) + for (let loop = 0; loop < 2; loop++) { + for (let i = 0; i < IMAGES_COUNT; i++) { + const img = document.createElement('img'); + img.src = getImageUrl(i); + img.alt = `Parnaíba - Foto ${i + 1}`; + img.loading = 'lazy'; + container.appendChild(img); + } } }); \ No newline at end of file diff --git a/templates/base.j2 b/templates/base.j2 index 5c7b3e9..cf98ac0 100644 --- a/templates/base.j2 +++ b/templates/base.j2 @@ -6,11 +6,12 @@ {% set event_state = event.location.state if event and event.location and event.location.state else (state | default('')) %} {% set event_desc = event.description if event and event.description else (event_description | default('')) %} {% set event_url = event.url if event and event.url else ('https://' ~ event_edition ~ '.pythonnordeste.org/') %} -{% set event_preview_image = theme.preview_image if theme and theme.preview_image else (preview_image | default('static/img/icons/palm_tree.png')) %} -{% set event_favicon = theme.favicon if theme and theme.favicon else (favicon | default('static/img/icons/palm_tree.png')) %} +{% set event_preview_image = theme.preview_image if theme and theme.preview_image else (preview_image | default('static/img/logo_pyne2027.png')) %} +{% set event_favicon = theme.favicon if theme and theme.favicon else (favicon | default('static/img/favicon.png')) %} +{% set is_indexing_allowed = (environment != 'staging') and (allow_indexing | default(true)) %} - - {% if google_tag %} + + {% if google_tag and is_indexing_allowed %}