Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions .github/workflows/ci-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,6 @@ jobs:
- name: Checkout do código
uses: actions/checkout@v4

- name: Instalar Node.js
uses: actions/setup-node@v4
with:
node-version: '22'

- name: Testar imagens responsivas e carregamento progressivo
run: node --test tests/test_images.mjs

- name: Instalar uv
uses: astral-sh/setup-uv@v5
with:
Expand Down
8 changes: 0 additions & 8 deletions .github/workflows/ci-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,6 @@ jobs:
- name: Checkout do código
uses: actions/checkout@v4

- name: Instalar Node.js
uses: actions/setup-node@v4
with:
node-version: '22'

- name: Testar imagens responsivas e carregamento progressivo
run: node --test tests/test_images.mjs

- name: Instalar uv
uses: astral-sh/setup-uv@v5
with:
Expand Down
48 changes: 15 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,39 +62,21 @@ O projeto utiliza o **[uv](https://docs.astral.sh/uv/)** para gerenciamento de d
| `uv run ruff check` | Analisa e valida a qualidade do código Python |
| `uv run ruff format` | Formata o código Python automaticamente |

## Imagens do carrossel

O carrossel usa WebP em 480 e 800 px, com JPEG como fallback. As dimensões
são reservadas antes do download para evitar deslocamentos de layout. O
carregamento começa quando a galeria se aproxima da tela e antecipa somente
as fotos próximas da área visível. A animação pausa enquanto a galeria está longe.
O logo do primeiro viewport tem prioridade alta; as fotos não recebem preload.

Para regenerar as variantes, instale Node.js e FFmpeg (incluindo `ffprobe`):

```bash
node scripts/optimize_images.mjs
node --test tests/test_images.mjs
uv run main render
```

Para usar fotos originais de maior qualidade, mantenha os nomes de `00.jpg`
a `22.jpg` em uma pasta externa e passe o caminho ao gerador:

```bash
node scripts/optimize_images.mjs /caminho/para/originais
```

O gerador não altera os JPEG existentes nem amplia as imagens. Versione os
WebP e o manifesto gerados junto com o código. Os JPEG atuais já passaram por
redução e recompressão; gerar novas variantes não recupera detalhes perdidos.

Os nomes WebP incluem um hash do conteúdo: mudar a foto muda a URL. O GitHub
Pages controla os cabeçalhos HTTP e não permite definir `Cache-Control` por
arquivo. Caso uma CDN ou outro servidor seja configurado, aplique
`Cache-Control: public, max-age=31536000, immutable` somente aos WebP com hash.
HTML, JavaScript e o manifesto devem revalidar o cache, pois suas URLs não
são versionadas. Não configure cache imutável para toda a pasta `static/`.
## Carrossel de atrações

A seção "Parnaíba espera por você" mostra um carrossel com foto e descrição das
principais atrações da cidade e da região. Para editar os textos, a ordem ou as
fotos, altere `destination.highlights` em `variables.yaml`; cada item aponta
para uma foto JPEG de `static/img/city/` pelo número (`"01"` → `01.jpg`).

Os slides são gerados no HTML (bons para SEO e funcionam sem JavaScript, como
uma faixa rolável), e as fotos usam carregamento lazy nativo do navegador. O
JavaScript adiciona setas, indicadores, navegação pelo teclado e troca
automática, que pausa fora da tela, ao passar o mouse, ao focar e pelo botão de
pausa. Quem prefere menos movimento começa com ela pausada.

Ao adicionar fotos, comprima-as antes (JPEG com qualidade ~75 e até ~1200 px de
largura é suficiente).

---

Expand Down
18 changes: 18 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
templates_folder = Path(__file__).parent / "templates"
static_folder = Path(__file__).parent / "static"
output_folder = Path(__file__).parent / "dist"
city_photos_folder = static_folder / "img" / "city"


cli = Typer(no_args_is_help=True)
Expand All @@ -28,6 +29,22 @@ def load_settings() -> dict:
return {}


def load_destination_slides(settings: dict) -> list[dict]:
"""Associa cada item de destination.highlights à sua foto em static/img/city/."""
highlights = (settings.get("destination") or {}).get("highlights") or []
slides = []
for highlight in highlights:
photo = city_photos_folder / f"{str(highlight['photo']).zfill(2)}.jpg"
if not photo.exists():
raise ValueError(
f"Foto '{photo.name}' de destination.highlights não existe em {city_photos_folder}."
)
slides.append(
{**highlight, "image": photo.relative_to(Path(__file__).parent).as_posix()}
)
return slides


def write_sitemap(site_url: str) -> None:
"""Gera dist/sitemap.xml com as páginas renderizadas (referenciado no robots.txt)."""
lastmod = datetime.now(UTC).date().isoformat()
Expand Down Expand Up @@ -55,6 +72,7 @@ def write_sitemap(site_url: str) -> None:
def render():
"""Gera os arquivos HTML estáticos na pasta dist."""
settings = load_settings()
settings["destination_slides"] = load_destination_slides(settings)

if output_folder.exists():
shutil.rmtree(output_folder)
Expand Down
126 changes: 120 additions & 6 deletions pages/index.j2
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% extends "templates/base.j2" %}

{% block extra_css %}
<link rel="stylesheet" href="./static/css/phb_photos_animation.css">
<link rel="stylesheet" href="./static/css/destination_carousel.css">
{% endblock %}

{% block content %}
Expand Down Expand Up @@ -46,6 +46,34 @@
{% endif %}
</div>

<!-- Data e Local do Evento -->
{% set location = event.location or {} %}
{% if event.date or location.venue %}
<ul class="event-details hero-animate-3" aria-label="Data e local do evento">
{% if event.date %}
<li class="event-detail">
<i class="fa-solid fa-calendar-days" aria-hidden="true"></i>
{% if event.start_date %}
<time datetime="{{ event.start_date }}{% if event.end_date %}/{{ event.end_date }}{% endif %}">{{ event.date }}</time>
{% else %}
<span>{{ event.date }}</span>
{% endif %}
</li>
{% endif %}
{% if location.venue %}
{% set venue_text = location.venue ~ ((', em ' ~ location.city ~ ', ' ~ (location.state_code or location.state)) if location.city else '') %}
<li class="event-detail">
<i class="fa-solid fa-building-columns" aria-hidden="true"></i>
{% if location.map_url %}
<a href="{{ location.map_url }}" target="_blank" rel="noopener noreferrer"{% if location.venue_full_name %} title="{{ location.venue_full_name }}"{% endif %}>{{ venue_text }}</a>
{% else %}
<span{% if location.venue_full_name %} title="{{ location.venue_full_name }}"{% endif %}>{{ venue_text }}</span>
{% endif %}
</li>
{% endif %}
</ul>
{% endif %}

<!-- Botões de Ação Condicionais -->
{% if actions and (actions.tickets_url or actions.call_for_papers_url) %}
<div class="action-buttons hero-animate-4">
Expand Down Expand Up @@ -97,7 +125,7 @@
</div>
</section>

<!-- Galeria de Fotos de Parnaíba -->
<!-- Carrossel de atrações de Parnaíba e região -->
<section id="destination" class="destination-section">
<div class="section-header">
<span class="section-tag"><i class="fa-solid fa-compass" aria-hidden="true"></i> Destino {{ event.edition }}</span>
Expand All @@ -107,16 +135,102 @@
</p>
</div>

<div class="phb-photos" id="phb-photos">
<div class="phb-photos-carousel">
<!-- Imagens injetadas via JavaScript -->
{% if destination_slides %}
<div class="destination-carousel" id="destination-carousel" role="region" aria-roledescription="carrossel" aria-label="Atrações de Parnaíba e região">
<div class="carousel-viewport">
<ul class="carousel-track" tabindex="0" aria-label="Fotos e descrições das atrações">
{% for slide in destination_slides %}
<li class="carousel-slide" id="destination-slide-{{ loop.index }}" role="group" aria-roledescription="slide" aria-label="{{ loop.index }} de {{ loop.length }}: {{ slide.title }}">
<figure class="slide-card">
<div class="slide-media">
<img src="{{ slide.image }}" alt="{{ slide.alt or slide.title }}" loading="lazy" decoding="async">
</div>
<figcaption class="slide-caption">
<span class="slide-counter" aria-hidden="true">{{ '%02d' % loop.index }} / {{ '%02d' % loop.length }}</span>
<h3 class="slide-title">{{ slide.title }}</h3>
<p class="slide-description">{{ slide.description }}</p>
</figcaption>
</figure>
</li>
{% endfor %}
</ul>
</div>

<!-- Controles: exibidos apenas quando o JavaScript está ativo -->
<div class="carousel-controls" hidden>
<button type="button" class="carousel-button" data-carousel-prev aria-controls="destination-carousel" aria-label="Atração anterior">
<i class="fa-solid fa-chevron-left" aria-hidden="true"></i>
</button>
<div class="carousel-dots">
{% for slide in destination_slides %}
<button type="button" class="carousel-dot" data-carousel-dot="{{ loop.index0 }}" aria-controls="destination-slide-{{ loop.index }}" aria-label="Ir para {{ slide.title }}"></button>
{% endfor %}
</div>
<button type="button" class="carousel-button" data-carousel-next aria-controls="destination-carousel" aria-label="Próxima atração">
<i class="fa-solid fa-chevron-right" aria-hidden="true"></i>
</button>
<button type="button" class="carousel-button carousel-toggle" data-carousel-toggle aria-label="Pausar troca automática">
<i class="fa-solid fa-pause" aria-hidden="true"></i>
</button>
</div>
</div>
{% endif %}
</section>

<!-- Local do Evento: foto e mapa -->
{% set location = event.location or {} %}
{% if location.venue %}
<section id="venue" class="venue-section">
<div class="section-header">
<span class="section-tag"><i class="fa-solid fa-location-dot" aria-hidden="true"></i> Local do evento</span>
<h2 class="section-title"><i class="fa-solid fa-building-columns" aria-hidden="true"></i> {{ location.venue }}</h2>
{% if location.venue_full_name %}
<p class="section-subtitle">{{ location.venue_full_name }}</p>
{% endif %}
</div>

<div class="venue-grid">
<figure class="venue-photo{% if not location.photo %} is-placeholder{% endif %}">
{% if location.photo %}
<img src="{{ location.photo }}" alt="{{ location.photo_alt or ('Foto da ' ~ location.venue) }}" loading="lazy" decoding="async">
{% else %}
<div class="venue-photo-placeholder">
<i class="fa-solid fa-camera" aria-hidden="true"></i>
<span>Foto do local em breve</span>
</div>
{% endif %}
</figure>

{% set map_query = location.map_query or (location.venue ~ ', ' ~ location.city ~ ' - ' ~ (location.state_code or location.state)) %}
<div class="venue-map">
<iframe
src="https://www.google.com/maps?q={{ map_query | urlencode }}&amp;output=embed"
title="Mapa com a localização da {{ location.venue }}"
loading="lazy"
referrerpolicy="no-referrer-when-downgrade"
allowfullscreen></iframe>
</div>
</div>

<div class="venue-info">
<p class="venue-address">
<i class="fa-solid fa-map-pin" aria-hidden="true"></i>
<span>
{% if location.address %}{{ location.address }}, {% endif %}{{ location.city }} - {{ location.state_code or location.state }}
</span>
</p>
{% if location.map_url %}
<a href="{{ location.map_url }}" target="_blank" rel="noopener noreferrer" class="venue-directions">
<i class="fa-solid fa-diamond-turn-right" aria-hidden="true"></i> Como chegar
</a>
{% endif %}
</div>
</section>
{% endif %}
</main>
{% endblock %}

{% block extra_js %}
<script src="./static/js/phb_photos_animation.js"></script>
<script src="./static/js/destination_carousel.js"></script>
<script src="./static/js/hero_animations.js"></script>
{% endblock %}
45 changes: 0 additions & 45 deletions scripts/optimize_images.mjs

This file was deleted.

Loading
Loading