diff --git a/.github/workflows/ci-dev.yml b/.github/workflows/ci-dev.yml index 467ce3b..424c8d9 100644 --- a/.github/workflows/ci-dev.yml +++ b/.github/workflows/ci-dev.yml @@ -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: diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index 2f67c03..ff10820 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -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: diff --git a/README.md b/README.md index c9a4543..d2bc0fc 100644 --- a/README.md +++ b/README.md @@ -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). --- diff --git a/main.py b/main.py index 761ea81..b2621c3 100644 --- a/main.py +++ b/main.py @@ -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) @@ -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() @@ -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) diff --git a/pages/index.j2 b/pages/index.j2 index e281948..d0bc4c1 100644 --- a/pages/index.j2 +++ b/pages/index.j2 @@ -1,7 +1,7 @@ {% extends "templates/base.j2" %} {% block extra_css %} - + {% endblock %} {% block content %} @@ -46,6 +46,34 @@ {% endif %} + + {% set location = event.location or {} %} + {% if event.date or location.venue %} + + {% endif %} + {% if actions and (actions.tickets_url or actions.call_for_papers_url) %}
@@ -97,7 +125,7 @@
- +
@@ -107,16 +135,102 @@

-
-
+ + + {% set location = event.location or {} %} + {% if location.venue %} +
+
+ +

{{ location.venue }}

+ {% if location.venue_full_name %} +

{{ location.venue_full_name }}

+ {% endif %} +
+ +
+
+ {% if location.photo %} + {{ location.photo_alt or ('Foto da ' ~ location.venue) }} + {% else %} +
+ + Foto do local em breve +
+ {% endif %} +
+ + {% set map_query = location.map_query or (location.venue ~ ', ' ~ location.city ~ ' - ' ~ (location.state_code or location.state)) %} +
+
+ +
+

+ + + {% if location.address %}{{ location.address }}, {% endif %}{{ location.city }} - {{ location.state_code or location.state }} + +

+ {% if location.map_url %} + + Como chegar + + {% endif %} +
+ {% endif %} {% endblock %} {% block extra_js %} - + {% endblock %} \ No newline at end of file diff --git a/scripts/optimize_images.mjs b/scripts/optimize_images.mjs deleted file mode 100644 index 7ed9f44..0000000 --- a/scripts/optimize_images.mjs +++ /dev/null @@ -1,45 +0,0 @@ -import { createHash } from 'node:crypto'; -import { execFileSync } from 'node:child_process'; -import { mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'; -import { dirname, join, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); -const output = join(root, 'static/img/background'); -const input = process.argv[2] ? resolve(process.argv[2]) : output; -const filenames = readdirSync(input).filter(name => /^\d{2}\.jpg$/.test(name)).sort(); -if (filenames.length !== 23) throw new Error('Expected 23 JPEG images (00.jpg to 22.jpg).'); -mkdirSync(output, { recursive: true }); -const images = []; - -for (const filename of filenames) { - const original = join(input, filename); - const metadata = JSON.parse(execFileSync('ffprobe', [ - '-v', 'error', '-select_streams', 'v:0', '-show_entries', - 'stream=width,height', '-of', 'json', original, - ], { encoding: 'utf8' })).streams[0]; - const variants = []; - const widths = [...new Set([Math.min(480, metadata.width), Math.min(800, metadata.width)])]; - for (const width of widths) { - const temporary = join(output, `${filename.slice(0, -4)}-${width}.tmp.webp`); - execFileSync('ffmpeg', [ - '-hide_banner', '-loglevel', 'error', '-y', '-i', original, - '-vf', `scale=w=${width}:h=-2:flags=lanczos`, '-frames:v', '1', - '-c:v', 'libwebp', '-quality', '75', '-compression_level', '6', - '-map_metadata', '-1', temporary, - ]); - const hash = createHash('sha256').update(readFileSync(temporary)).digest('hex').slice(0, 12); - const name = `${filename.slice(0, -4)}-${width}.${hash}.webp`; - renameSync(temporary, join(output, name)); - variants.push({ width, url: `static/img/background/${name}` }); - } - images.push({ - width: metadata.width, - height: metadata.height, - fallback: `static/img/background/${filename}`, - variants, - }); -} - -writeFileSync(join(output, 'manifest.json'), `${JSON.stringify(images)}\n`); -console.log(`Generated responsive WebP variants for ${images.length} images.`); \ No newline at end of file diff --git a/static/css/destination_carousel.css b/static/css/destination_carousel.css new file mode 100644 index 0000000..5ecebea --- /dev/null +++ b/static/css/destination_carousel.css @@ -0,0 +1,224 @@ +/* ========================================================================== + Carrossel de Atrações (Parnaíba e região) + Sem JavaScript, funciona como uma faixa rolável com scroll-snap. + ========================================================================== */ + +.destination-carousel { + max-width: 1100px; + margin: 0 auto; + text-align: left; +} + +.carousel-viewport { + border-radius: 24px; + box-shadow: 0 12px 32px rgba(43, 27, 34, 0.12); +} + +.carousel-track { + list-style: none; + display: flex; + overflow-x: auto; + scroll-snap-type: x mandatory; + overscroll-behavior-x: contain; + scrollbar-width: none; + border-radius: 24px; +} + +.carousel-track::-webkit-scrollbar { + display: none; +} + +.carousel-track:focus-visible { + outline: 3px solid var(--secondary-color); + outline-offset: 4px; +} + +.carousel-slide { + flex: 0 0 100%; + scroll-snap-align: start; + scroll-snap-stop: always; +} + +.slide-card { + display: grid; + grid-template-columns: minmax(0, 1fr); + height: 100%; + background: #FFFFFF; + overflow: hidden; +} + +.slide-media { + display: block; + aspect-ratio: 16 / 10; + background: #E9DFD3; +} + +.slide-media img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + +.slide-caption { + display: flex; + flex-direction: column; + justify-content: center; + gap: 0.6rem; + padding: 1.5rem 1.5rem 1.75rem; +} + +.slide-counter { + font-family: var(--font-display); + font-size: 0.85rem; + font-weight: 700; + letter-spacing: 0.12em; + color: var(--secondary-color); +} + +.slide-title { + font-family: var(--font-display); + font-size: clamp(1.4rem, 2.6vw, 1.9rem); + font-weight: 800; + line-height: 1.15; + letter-spacing: -0.01em; + color: var(--text-dark); +} + +.slide-description { + font-size: 1rem; + line-height: 1.65; + color: #55444D; +} + +@media (min-width: 900px) { + .slide-card { + grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); + } + + .slide-media { + aspect-ratio: auto; + min-height: 420px; + } + + .slide-caption { + padding: 2.5rem 2.75rem; + } + + .slide-description { + font-size: 1.05rem; + } +} + +/* Controles */ + +.carousel-controls { + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + margin-top: 1.5rem; +} + +.carousel-controls[hidden] { + display: none; +} + +.carousel-button { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 44px; + height: 44px; + border: 2px solid var(--secondary-color); + border-radius: 50%; + background: #FFFFFF; + color: var(--secondary-color); + font-size: 1rem; + cursor: pointer; + transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease; +} + +@media (hover: hover) { + .carousel-button:hover { + background: var(--secondary-color); + color: #FFFFFF; + transform: translateY(-2px); + } + + .carousel-dot:hover::before { + background: var(--primary-color); + } +} + +.carousel-button:focus-visible, +.carousel-dot:focus-visible { + outline: 3px solid var(--text-dark); + outline-offset: 3px; +} + +.carousel-toggle { + margin-left: 0.5rem; + border-color: rgba(43, 27, 34, 0.25); + color: var(--text-dark); +} + +.carousel-dots { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.15rem; +} + +/* Área de toque de 24px com indicador visual menor */ +.carousel-dot { + position: relative; + width: 24px; + height: 24px; + border: 0; + background: none; + border-radius: 50%; + cursor: pointer; +} + +.carousel-dot::before { + content: ""; + position: absolute; + inset: 8px; + border-radius: 9999px; + background: rgba(43, 27, 34, 0.25); + transition: background 0.2s ease, inset 0.2s ease; +} + +.carousel-dot[aria-current="true"]::before { + inset: 6px; + background: var(--secondary-color); +} + +@media (max-width: 600px) { + .carousel-controls { + flex-wrap: wrap; + gap: 0.5rem; + } + + .carousel-dots { + order: -1; + flex-basis: 100%; + gap: 0; + } + + .slide-caption { + padding: 1.25rem 1.25rem 1.5rem; + } +} + +@media (prefers-reduced-motion: reduce) { + .carousel-button { + transition: none; + } + + .carousel-button:hover { + transform: none; + } +} diff --git a/static/css/main.css b/static/css/main.css index e6b0f9b..23383b6 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -199,6 +199,46 @@ body { color: rgba(255, 255, 255, 0.95); } +/* Data e Local do Evento */ +.event-details { + list-style: none; + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.75rem; + margin: -0.5rem 0 2.25rem; +} + +.event-detail { + display: inline-flex; + align-items: center; + gap: 0.6rem; + padding: 0.6rem 1.25rem; + border-radius: 14px; + background: rgba(43, 27, 34, 0.18); + border: 1px solid rgba(255, 255, 255, 0.3); + font-size: 1.05rem; + font-weight: 700; + color: #FFFFFF; +} + +.event-detail i { + font-size: 1.1rem; + color: var(--accent-color); +} + +.event-detail a { + color: inherit; + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 4px; +} + +.event-detail a:hover, +.event-detail a:focus-visible { + text-decoration-thickness: 2px; +} + /* Botões de Ação */ .action-buttons { display: flex; @@ -322,6 +362,125 @@ body { } +/* ========================================================================== + Venue Section (Local do Evento) + ========================================================================== */ + +.venue-section { + padding: 5rem 1.5rem 4rem; + background: #FFFFFF; + text-align: center; +} + +.venue-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1.5rem; + max-width: 1100px; + margin: 0 auto; +} + +.venue-photo, +.venue-map { + min-height: 360px; + border-radius: 24px; + overflow: hidden; + box-shadow: 0 12px 32px rgba(43, 27, 34, 0.12); +} + +.venue-photo img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + +.venue-photo.is-placeholder { + box-shadow: none; + border: 2px dashed rgba(212, 99, 138, 0.45); + background: var(--accent-color); +} + +.venue-photo-placeholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.75rem; + height: 100%; + color: var(--secondary-color); + font-weight: 600; +} + +.venue-photo-placeholder i { + font-size: 2.25rem; +} + +.venue-map iframe { + display: block; + width: 100%; + height: 100%; + min-height: 360px; + border: 0; +} + +.venue-info { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: 1rem 1.5rem; + max-width: 1100px; + margin: 2rem auto 0; +} + +.venue-address { + display: inline-flex; + align-items: center; + gap: 0.6rem; + font-size: 1.05rem; + color: #55444D; +} + +.venue-address i { + color: var(--secondary-color); +} + +.venue-directions { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.7rem 1.5rem; + border-radius: 9999px; + background: var(--secondary-color); + color: #FFFFFF; + font-weight: 700; + text-decoration: none; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.venue-directions:hover { + transform: translateY(-2px); + box-shadow: 0 8px 18px rgba(212, 99, 138, 0.35); +} + +.venue-directions:focus-visible { + outline: 3px solid var(--text-dark); + outline-offset: 3px; +} + +@media (max-width: 800px) { + .venue-grid { + grid-template-columns: minmax(0, 1fr); + } + + .venue-photo, + .venue-map, + .venue-map iframe { + min-height: 260px; + } +} + /* ========================================================================== Destination Section (Galeria de Parnaíba) ========================================================================== */ @@ -362,17 +521,6 @@ body { 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 ========================================================================== */ @@ -478,9 +626,15 @@ body { font-size: 0.85rem; } - .destination-section { + .destination-section, + .venue-section { padding: 3.5rem 1rem 2.5rem; } + + .event-detail { + font-size: 0.95rem; + padding: 0.5rem 1rem; + } } /* ========================================================================== diff --git a/static/css/phb_photos_animation.css b/static/css/phb_photos_animation.css deleted file mode 100644 index c5435ea..0000000 --- a/static/css/phb_photos_animation.css +++ /dev/null @@ -1,67 +0,0 @@ -.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 60s linear infinite; - animation-play-state: paused; - will-change: transform; -} - -.phb-photos-carousel.is-visible { - animation-play-state: running; -} - -.phb-photos:hover .phb-photos-carousel { - animation-play-state: paused; -} - -.phb-photos-carousel picture { - display: block; - height: 290px; - flex-shrink: 0; - 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 { - display: block; - width: 100%; - height: 100%; - object-fit: cover; - border-radius: inherit; -} - -.phb-photos-carousel picture:hover { - transform: scale(1.05) translateY(-4px); - box-shadow: 0 16px 32px rgba(43, 27, 34, 0.24); -} - -@keyframes phb-photos-scroll { - from { - transform: translateX(0); - } - to { - transform: translateX(-50%); - } -} - -@media (prefers-reduced-motion: reduce) { - .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/img/background/00-480.729d05cf0f68.webp b/static/img/background/00-480.729d05cf0f68.webp deleted file mode 100644 index da1ff48..0000000 Binary files a/static/img/background/00-480.729d05cf0f68.webp and /dev/null differ diff --git a/static/img/background/00-800.1437ed8f24aa.webp b/static/img/background/00-800.1437ed8f24aa.webp deleted file mode 100644 index 041d3cc..0000000 Binary files a/static/img/background/00-800.1437ed8f24aa.webp and /dev/null differ diff --git a/static/img/background/01-480.b996d309585b.webp b/static/img/background/01-480.b996d309585b.webp deleted file mode 100644 index 0565329..0000000 Binary files a/static/img/background/01-480.b996d309585b.webp and /dev/null differ diff --git a/static/img/background/01-800.be196df5cbf6.webp b/static/img/background/01-800.be196df5cbf6.webp deleted file mode 100644 index 192a70e..0000000 Binary files a/static/img/background/01-800.be196df5cbf6.webp and /dev/null differ diff --git a/static/img/background/02-480.710d2c81e1fb.webp b/static/img/background/02-480.710d2c81e1fb.webp deleted file mode 100644 index 98b8c15..0000000 Binary files a/static/img/background/02-480.710d2c81e1fb.webp and /dev/null differ diff --git a/static/img/background/02-800.0f3672e0f7ca.webp b/static/img/background/02-800.0f3672e0f7ca.webp deleted file mode 100644 index 5ed6cb9..0000000 Binary files a/static/img/background/02-800.0f3672e0f7ca.webp and /dev/null differ diff --git a/static/img/background/03-480.1629394cd283.webp b/static/img/background/03-480.1629394cd283.webp deleted file mode 100644 index 119b86d..0000000 Binary files a/static/img/background/03-480.1629394cd283.webp and /dev/null differ diff --git a/static/img/background/03-800.3daf162f84de.webp b/static/img/background/03-800.3daf162f84de.webp deleted file mode 100644 index 985b6e7..0000000 Binary files a/static/img/background/03-800.3daf162f84de.webp and /dev/null differ diff --git a/static/img/background/04-480.77283cad8582.webp b/static/img/background/04-480.77283cad8582.webp deleted file mode 100644 index 83994d9..0000000 Binary files a/static/img/background/04-480.77283cad8582.webp and /dev/null differ diff --git a/static/img/background/04-800.c3ed198c729e.webp b/static/img/background/04-800.c3ed198c729e.webp deleted file mode 100644 index 5c56a9c..0000000 Binary files a/static/img/background/04-800.c3ed198c729e.webp and /dev/null differ diff --git a/static/img/background/05-480.8d393628b8cd.webp b/static/img/background/05-480.8d393628b8cd.webp deleted file mode 100644 index 601b9a7..0000000 Binary files a/static/img/background/05-480.8d393628b8cd.webp and /dev/null differ diff --git a/static/img/background/05-800.7b9c9100dbe7.webp b/static/img/background/05-800.7b9c9100dbe7.webp deleted file mode 100644 index 295bb21..0000000 Binary files a/static/img/background/05-800.7b9c9100dbe7.webp and /dev/null differ diff --git a/static/img/background/06-480.317698cd0df8.webp b/static/img/background/06-480.317698cd0df8.webp deleted file mode 100644 index a4c8e2b..0000000 Binary files a/static/img/background/06-480.317698cd0df8.webp and /dev/null differ diff --git a/static/img/background/06-800.a41e64be605b.webp b/static/img/background/06-800.a41e64be605b.webp deleted file mode 100644 index 59934cb..0000000 Binary files a/static/img/background/06-800.a41e64be605b.webp and /dev/null differ diff --git a/static/img/background/07-480.2237e9343b01.webp b/static/img/background/07-480.2237e9343b01.webp deleted file mode 100644 index cde0e74..0000000 Binary files a/static/img/background/07-480.2237e9343b01.webp and /dev/null differ diff --git a/static/img/background/07-800.f048fcd993fb.webp b/static/img/background/07-800.f048fcd993fb.webp deleted file mode 100644 index cd9e4ba..0000000 Binary files a/static/img/background/07-800.f048fcd993fb.webp and /dev/null differ diff --git a/static/img/background/08-480.489d8554feca.webp b/static/img/background/08-480.489d8554feca.webp deleted file mode 100644 index 39a660f..0000000 Binary files a/static/img/background/08-480.489d8554feca.webp and /dev/null differ diff --git a/static/img/background/08-800.37e4b65bec0b.webp b/static/img/background/08-800.37e4b65bec0b.webp deleted file mode 100644 index 5eeea34..0000000 Binary files a/static/img/background/08-800.37e4b65bec0b.webp and /dev/null differ diff --git a/static/img/background/09-480.df47d561348e.webp b/static/img/background/09-480.df47d561348e.webp deleted file mode 100644 index eb85a68..0000000 Binary files a/static/img/background/09-480.df47d561348e.webp and /dev/null differ diff --git a/static/img/background/09-800.0d8c91262999.webp b/static/img/background/09-800.0d8c91262999.webp deleted file mode 100644 index 630add0..0000000 Binary files a/static/img/background/09-800.0d8c91262999.webp and /dev/null differ diff --git a/static/img/background/10-480.61988362132b.webp b/static/img/background/10-480.61988362132b.webp deleted file mode 100644 index 419c667..0000000 Binary files a/static/img/background/10-480.61988362132b.webp and /dev/null differ diff --git a/static/img/background/10-800.d3362e6bafb7.webp b/static/img/background/10-800.d3362e6bafb7.webp deleted file mode 100644 index 24beece..0000000 Binary files a/static/img/background/10-800.d3362e6bafb7.webp and /dev/null differ diff --git a/static/img/background/11-480.8aa829dbc2d0.webp b/static/img/background/11-480.8aa829dbc2d0.webp deleted file mode 100644 index c65ca44..0000000 Binary files a/static/img/background/11-480.8aa829dbc2d0.webp and /dev/null differ diff --git a/static/img/background/11-800.35a102910aef.webp b/static/img/background/11-800.35a102910aef.webp deleted file mode 100644 index c297e90..0000000 Binary files a/static/img/background/11-800.35a102910aef.webp and /dev/null differ diff --git a/static/img/background/12-480.b3ff879be359.webp b/static/img/background/12-480.b3ff879be359.webp deleted file mode 100644 index c8e1de8..0000000 Binary files a/static/img/background/12-480.b3ff879be359.webp and /dev/null differ diff --git a/static/img/background/12-800.42d5297332b5.webp b/static/img/background/12-800.42d5297332b5.webp deleted file mode 100644 index eed75a6..0000000 Binary files a/static/img/background/12-800.42d5297332b5.webp and /dev/null differ diff --git a/static/img/background/13-480.1d477c4d2930.webp b/static/img/background/13-480.1d477c4d2930.webp deleted file mode 100644 index 7182656..0000000 Binary files a/static/img/background/13-480.1d477c4d2930.webp and /dev/null differ diff --git a/static/img/background/13-800.7e1e3f4659c9.webp b/static/img/background/13-800.7e1e3f4659c9.webp deleted file mode 100644 index 7baefda..0000000 Binary files a/static/img/background/13-800.7e1e3f4659c9.webp and /dev/null differ diff --git a/static/img/background/14-480.4cfad57591a8.webp b/static/img/background/14-480.4cfad57591a8.webp deleted file mode 100644 index 9a02e05..0000000 Binary files a/static/img/background/14-480.4cfad57591a8.webp and /dev/null differ diff --git a/static/img/background/14-800.e46dbf7a13d6.webp b/static/img/background/14-800.e46dbf7a13d6.webp deleted file mode 100644 index db4f995..0000000 Binary files a/static/img/background/14-800.e46dbf7a13d6.webp and /dev/null differ diff --git a/static/img/background/15-480.5e3bfea93154.webp b/static/img/background/15-480.5e3bfea93154.webp deleted file mode 100644 index fadd9ea..0000000 Binary files a/static/img/background/15-480.5e3bfea93154.webp and /dev/null differ diff --git a/static/img/background/15-800.e8f1e354ac81.webp b/static/img/background/15-800.e8f1e354ac81.webp deleted file mode 100644 index 60003c5..0000000 Binary files a/static/img/background/15-800.e8f1e354ac81.webp and /dev/null differ diff --git a/static/img/background/16-480.488f80c4b196.webp b/static/img/background/16-480.488f80c4b196.webp deleted file mode 100644 index 9e912f9..0000000 Binary files a/static/img/background/16-480.488f80c4b196.webp and /dev/null differ diff --git a/static/img/background/16-800.640620cb534d.webp b/static/img/background/16-800.640620cb534d.webp deleted file mode 100644 index 67f2863..0000000 Binary files a/static/img/background/16-800.640620cb534d.webp and /dev/null differ diff --git a/static/img/background/17-480.103238406599.webp b/static/img/background/17-480.103238406599.webp deleted file mode 100644 index f3f0bca..0000000 Binary files a/static/img/background/17-480.103238406599.webp and /dev/null differ diff --git a/static/img/background/17-800.418fe4e8b8ec.webp b/static/img/background/17-800.418fe4e8b8ec.webp deleted file mode 100644 index 7d62caa..0000000 Binary files a/static/img/background/17-800.418fe4e8b8ec.webp and /dev/null differ diff --git a/static/img/background/18-480.8be073eab150.webp b/static/img/background/18-480.8be073eab150.webp deleted file mode 100644 index ca88739..0000000 Binary files a/static/img/background/18-480.8be073eab150.webp and /dev/null differ diff --git a/static/img/background/18-800.3fc02f2790bf.webp b/static/img/background/18-800.3fc02f2790bf.webp deleted file mode 100644 index 4d6bac4..0000000 Binary files a/static/img/background/18-800.3fc02f2790bf.webp and /dev/null differ diff --git a/static/img/background/19-480.a08f24f396b2.webp b/static/img/background/19-480.a08f24f396b2.webp deleted file mode 100644 index bbf3d50..0000000 Binary files a/static/img/background/19-480.a08f24f396b2.webp and /dev/null differ diff --git a/static/img/background/19-800.ce36b378a89f.webp b/static/img/background/19-800.ce36b378a89f.webp deleted file mode 100644 index 3a42d4a..0000000 Binary files a/static/img/background/19-800.ce36b378a89f.webp and /dev/null differ diff --git a/static/img/background/20-480.2a8e5f028915.webp b/static/img/background/20-480.2a8e5f028915.webp deleted file mode 100644 index e6d443a..0000000 Binary files a/static/img/background/20-480.2a8e5f028915.webp and /dev/null differ diff --git a/static/img/background/20-800.8c10a70fe884.webp b/static/img/background/20-800.8c10a70fe884.webp deleted file mode 100644 index 3babb95..0000000 Binary files a/static/img/background/20-800.8c10a70fe884.webp and /dev/null differ diff --git a/static/img/background/21-480.ed08578084dc.webp b/static/img/background/21-480.ed08578084dc.webp deleted file mode 100644 index b37eb39..0000000 Binary files a/static/img/background/21-480.ed08578084dc.webp and /dev/null differ diff --git a/static/img/background/21-800.58c854085405.webp b/static/img/background/21-800.58c854085405.webp deleted file mode 100644 index 5cb6f3f..0000000 Binary files a/static/img/background/21-800.58c854085405.webp and /dev/null differ diff --git a/static/img/background/22-480.32fc09bdcf1e.webp b/static/img/background/22-480.32fc09bdcf1e.webp deleted file mode 100644 index c6a59c4..0000000 Binary files a/static/img/background/22-480.32fc09bdcf1e.webp and /dev/null differ diff --git a/static/img/background/22-800.6fd95d247359.webp b/static/img/background/22-800.6fd95d247359.webp deleted file mode 100644 index 2e3348a..0000000 Binary files a/static/img/background/22-800.6fd95d247359.webp and /dev/null differ diff --git a/static/img/background/manifest.json b/static/img/background/manifest.json deleted file mode 100644 index a1a7cc9..0000000 --- a/static/img/background/manifest.json +++ /dev/null @@ -1 +0,0 @@ -[{"width":800,"height":534,"fallback":"static/img/background/00.jpg","variants":[{"width":480,"url":"static/img/background/00-480.729d05cf0f68.webp"},{"width":800,"url":"static/img/background/00-800.1437ed8f24aa.webp"}]},{"width":800,"height":450,"fallback":"static/img/background/01.jpg","variants":[{"width":480,"url":"static/img/background/01-480.b996d309585b.webp"},{"width":800,"url":"static/img/background/01-800.be196df5cbf6.webp"}]},{"width":800,"height":450,"fallback":"static/img/background/02.jpg","variants":[{"width":480,"url":"static/img/background/02-480.710d2c81e1fb.webp"},{"width":800,"url":"static/img/background/02-800.0f3672e0f7ca.webp"}]},{"width":800,"height":534,"fallback":"static/img/background/03.jpg","variants":[{"width":480,"url":"static/img/background/03-480.1629394cd283.webp"},{"width":800,"url":"static/img/background/03-800.3daf162f84de.webp"}]},{"width":800,"height":422,"fallback":"static/img/background/04.jpg","variants":[{"width":480,"url":"static/img/background/04-480.77283cad8582.webp"},{"width":800,"url":"static/img/background/04-800.c3ed198c729e.webp"}]},{"width":800,"height":450,"fallback":"static/img/background/05.jpg","variants":[{"width":480,"url":"static/img/background/05-480.8d393628b8cd.webp"},{"width":800,"url":"static/img/background/05-800.7b9c9100dbe7.webp"}]},{"width":800,"height":534,"fallback":"static/img/background/06.jpg","variants":[{"width":480,"url":"static/img/background/06-480.317698cd0df8.webp"},{"width":800,"url":"static/img/background/06-800.a41e64be605b.webp"}]},{"width":800,"height":556,"fallback":"static/img/background/07.jpg","variants":[{"width":480,"url":"static/img/background/07-480.2237e9343b01.webp"},{"width":800,"url":"static/img/background/07-800.f048fcd993fb.webp"}]},{"width":800,"height":450,"fallback":"static/img/background/08.jpg","variants":[{"width":480,"url":"static/img/background/08-480.489d8554feca.webp"},{"width":800,"url":"static/img/background/08-800.37e4b65bec0b.webp"}]},{"width":800,"height":532,"fallback":"static/img/background/09.jpg","variants":[{"width":480,"url":"static/img/background/09-480.df47d561348e.webp"},{"width":800,"url":"static/img/background/09-800.0d8c91262999.webp"}]},{"width":800,"height":534,"fallback":"static/img/background/10.jpg","variants":[{"width":480,"url":"static/img/background/10-480.61988362132b.webp"},{"width":800,"url":"static/img/background/10-800.d3362e6bafb7.webp"}]},{"width":800,"height":426,"fallback":"static/img/background/11.jpg","variants":[{"width":480,"url":"static/img/background/11-480.8aa829dbc2d0.webp"},{"width":800,"url":"static/img/background/11-800.35a102910aef.webp"}]},{"width":800,"height":422,"fallback":"static/img/background/12.jpg","variants":[{"width":480,"url":"static/img/background/12-480.b3ff879be359.webp"},{"width":800,"url":"static/img/background/12-800.42d5297332b5.webp"}]},{"width":800,"height":424,"fallback":"static/img/background/13.jpg","variants":[{"width":480,"url":"static/img/background/13-480.1d477c4d2930.webp"},{"width":800,"url":"static/img/background/13-800.7e1e3f4659c9.webp"}]},{"width":800,"height":486,"fallback":"static/img/background/14.jpg","variants":[{"width":480,"url":"static/img/background/14-480.4cfad57591a8.webp"},{"width":800,"url":"static/img/background/14-800.e46dbf7a13d6.webp"}]},{"width":800,"height":508,"fallback":"static/img/background/15.jpg","variants":[{"width":480,"url":"static/img/background/15-480.5e3bfea93154.webp"},{"width":800,"url":"static/img/background/15-800.e8f1e354ac81.webp"}]},{"width":800,"height":534,"fallback":"static/img/background/16.jpg","variants":[{"width":480,"url":"static/img/background/16-480.488f80c4b196.webp"},{"width":800,"url":"static/img/background/16-800.640620cb534d.webp"}]},{"width":800,"height":534,"fallback":"static/img/background/17.jpg","variants":[{"width":480,"url":"static/img/background/17-480.103238406599.webp"},{"width":800,"url":"static/img/background/17-800.418fe4e8b8ec.webp"}]},{"width":800,"height":534,"fallback":"static/img/background/18.jpg","variants":[{"width":480,"url":"static/img/background/18-480.8be073eab150.webp"},{"width":800,"url":"static/img/background/18-800.3fc02f2790bf.webp"}]},{"width":800,"height":460,"fallback":"static/img/background/19.jpg","variants":[{"width":480,"url":"static/img/background/19-480.a08f24f396b2.webp"},{"width":800,"url":"static/img/background/19-800.ce36b378a89f.webp"}]},{"width":800,"height":450,"fallback":"static/img/background/20.jpg","variants":[{"width":480,"url":"static/img/background/20-480.2a8e5f028915.webp"},{"width":800,"url":"static/img/background/20-800.8c10a70fe884.webp"}]},{"width":800,"height":534,"fallback":"static/img/background/21.jpg","variants":[{"width":480,"url":"static/img/background/21-480.ed08578084dc.webp"},{"width":800,"url":"static/img/background/21-800.58c854085405.webp"}]},{"width":800,"height":534,"fallback":"static/img/background/22.jpg","variants":[{"width":480,"url":"static/img/background/22-480.32fc09bdcf1e.webp"},{"width":800,"url":"static/img/background/22-800.6fd95d247359.webp"}]}] diff --git a/static/img/background/00.jpg b/static/img/city/00.jpg similarity index 100% rename from static/img/background/00.jpg rename to static/img/city/00.jpg diff --git a/static/img/background/01.jpg b/static/img/city/01.jpg similarity index 100% rename from static/img/background/01.jpg rename to static/img/city/01.jpg diff --git a/static/img/background/02.jpg b/static/img/city/02.jpg similarity index 100% rename from static/img/background/02.jpg rename to static/img/city/02.jpg diff --git a/static/img/background/03.jpg b/static/img/city/03.jpg similarity index 100% rename from static/img/background/03.jpg rename to static/img/city/03.jpg diff --git a/static/img/background/04.jpg b/static/img/city/04.jpg similarity index 100% rename from static/img/background/04.jpg rename to static/img/city/04.jpg diff --git a/static/img/background/05.jpg b/static/img/city/05.jpg similarity index 100% rename from static/img/background/05.jpg rename to static/img/city/05.jpg diff --git a/static/img/background/06.jpg b/static/img/city/06.jpg similarity index 100% rename from static/img/background/06.jpg rename to static/img/city/06.jpg diff --git a/static/img/background/07.jpg b/static/img/city/07.jpg similarity index 100% rename from static/img/background/07.jpg rename to static/img/city/07.jpg diff --git a/static/img/background/08.jpg b/static/img/city/08.jpg similarity index 100% rename from static/img/background/08.jpg rename to static/img/city/08.jpg diff --git a/static/img/background/09.jpg b/static/img/city/09.jpg similarity index 100% rename from static/img/background/09.jpg rename to static/img/city/09.jpg diff --git a/static/img/background/10.jpg b/static/img/city/10.jpg similarity index 100% rename from static/img/background/10.jpg rename to static/img/city/10.jpg diff --git a/static/img/background/11.jpg b/static/img/city/11.jpg similarity index 100% rename from static/img/background/11.jpg rename to static/img/city/11.jpg diff --git a/static/img/background/12.jpg b/static/img/city/12.jpg similarity index 100% rename from static/img/background/12.jpg rename to static/img/city/12.jpg diff --git a/static/img/background/13.jpg b/static/img/city/13.jpg similarity index 100% rename from static/img/background/13.jpg rename to static/img/city/13.jpg diff --git a/static/img/background/14.jpg b/static/img/city/14.jpg similarity index 100% rename from static/img/background/14.jpg rename to static/img/city/14.jpg diff --git a/static/img/background/15.jpg b/static/img/city/15.jpg similarity index 100% rename from static/img/background/15.jpg rename to static/img/city/15.jpg diff --git a/static/img/background/16.jpg b/static/img/city/16.jpg similarity index 100% rename from static/img/background/16.jpg rename to static/img/city/16.jpg diff --git a/static/img/background/17.jpg b/static/img/city/17.jpg similarity index 100% rename from static/img/background/17.jpg rename to static/img/city/17.jpg diff --git a/static/img/background/18.jpg b/static/img/city/18.jpg similarity index 100% rename from static/img/background/18.jpg rename to static/img/city/18.jpg diff --git a/static/img/background/19.jpg b/static/img/city/19.jpg similarity index 100% rename from static/img/background/19.jpg rename to static/img/city/19.jpg diff --git a/static/img/background/20.jpg b/static/img/city/20.jpg similarity index 100% rename from static/img/background/20.jpg rename to static/img/city/20.jpg diff --git a/static/img/background/21.jpg b/static/img/city/21.jpg similarity index 100% rename from static/img/background/21.jpg rename to static/img/city/21.jpg diff --git a/static/img/background/22.jpg b/static/img/city/22.jpg similarity index 100% rename from static/img/background/22.jpg rename to static/img/city/22.jpg diff --git a/static/img/ufdpar.jpg b/static/img/ufdpar.jpg new file mode 100644 index 0000000..69c8661 Binary files /dev/null and b/static/img/ufdpar.jpg differ diff --git a/static/js/destination_carousel.js b/static/js/destination_carousel.js new file mode 100644 index 0000000..e97c89c --- /dev/null +++ b/static/js/destination_carousel.js @@ -0,0 +1,113 @@ +document.addEventListener('DOMContentLoaded', function() { + const carousel = document.getElementById('destination-carousel'); + if (!carousel) return; + + const track = carousel.querySelector('.carousel-track'); + const slides = Array.from(carousel.querySelectorAll('.carousel-slide')); + const dots = Array.from(carousel.querySelectorAll('[data-carousel-dot]')); + const controls = carousel.querySelector('.carousel-controls'); + const toggle = carousel.querySelector('[data-carousel-toggle]'); + if (!track || slides.length < 2) return; + + const AUTOPLAY_DELAY = 7000; + const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); + let current = 0; + let timer = null; + let isVisible = false; + let isHovered = false; + let hasFocus = false; + // Quem prefere menos movimento começa com a troca automática pausada. + let isPaused = reducedMotion.matches; + + const setActive = function(index) { + current = index; + dots.forEach(function(dot, dotIndex) { + if (dotIndex === index) dot.setAttribute('aria-current', 'true'); + else dot.removeAttribute('aria-current'); + }); + }; + + const goTo = function(index) { + const target = (index + slides.length) % slides.length; + track.scrollTo({ + left: slides[target].offsetLeft - track.offsetLeft, + behavior: reducedMotion.matches ? 'auto' : 'smooth', + }); + setActive(target); + }; + + const stop = function() { + clearInterval(timer); + timer = null; + }; + + const refreshAutoplay = function() { + const shouldRun = !isPaused && isVisible && !isHovered && !hasFocus && !document.hidden; + if (shouldRun && !timer) { + timer = setInterval(function() { goTo(current + 1); }, AUTOPLAY_DELAY); + } else if (!shouldRun) { + stop(); + } + }; + + const updateToggle = function() { + if (!toggle) return; + toggle.setAttribute('aria-label', isPaused ? 'Retomar troca automática' : 'Pausar troca automática'); + const icon = toggle.querySelector('i'); + icon.classList.toggle('fa-pause', !isPaused); + icon.classList.toggle('fa-play', isPaused); + }; + + carousel.querySelector('[data-carousel-prev]').addEventListener('click', function() { goTo(current - 1); }); + carousel.querySelector('[data-carousel-next]').addEventListener('click', function() { goTo(current + 1); }); + dots.forEach(function(dot) { + dot.addEventListener('click', function() { goTo(Number(dot.dataset.carouselDot)); }); + }); + if (toggle) { + toggle.addEventListener('click', function() { + isPaused = !isPaused; + updateToggle(); + refreshAutoplay(); + }); + } + + track.addEventListener('keydown', function(event) { + if (event.key === 'ArrowRight') goTo(current + 1); + else if (event.key === 'ArrowLeft') goTo(current - 1); + else return; + event.preventDefault(); + }); + + // Mantém os indicadores sincronizados quando a pessoa arrasta/rola o carrossel. + const slideObserver = new IntersectionObserver(function(entries) { + for (const entry of entries) { + if (entry.isIntersecting) setActive(slides.indexOf(entry.target)); + } + }, { root: track, threshold: 0.6 }); + slides.forEach(function(slide) { slideObserver.observe(slide); }); + + // Só troca automaticamente enquanto o carrossel está na tela. + new IntersectionObserver(function(entries) { + isVisible = entries[0].isIntersecting; + refreshAutoplay(); + }, { threshold: 0.4 }).observe(carousel); + + carousel.addEventListener('mouseenter', function() { isHovered = true; refreshAutoplay(); }); + carousel.addEventListener('mouseleave', function() { isHovered = false; refreshAutoplay(); }); + carousel.addEventListener('focusin', function() { hasFocus = true; refreshAutoplay(); }); + carousel.addEventListener('focusout', function(event) { + hasFocus = carousel.contains(event.relatedTarget); + refreshAutoplay(); + }); + track.addEventListener('pointerdown', function() { + isPaused = true; + updateToggle(); + refreshAutoplay(); + }); + document.addEventListener('visibilitychange', refreshAutoplay); + + controls.hidden = false; + carousel.classList.add('is-enhanced'); + setActive(0); + updateToggle(); +}); diff --git a/static/js/phb_photos_animation.js b/static/js/phb_photos_animation.js deleted file mode 100644 index 3b2d610..0000000 --- a/static/js/phb_photos_animation.js +++ /dev/null @@ -1,71 +0,0 @@ -document.addEventListener('DOMContentLoaded', async function() { - const container = document.querySelector('.phb-photos-carousel'); - if (!container) return; - - const response = await fetch('static/img/background/manifest.json'); - if (!response.ok) return; - const images = await response.json(); - const pictures = []; - const loadPicture = function(picture) { - const source = picture.querySelector('source'); - const img = picture.querySelector('img'); - source.srcset = source.dataset.srcset; - img.src = img.dataset.src; - }; - const observer = 'IntersectionObserver' in window - ? new IntersectionObserver(function(entries) { - for (const entry of entries) { - if (!entry.isIntersecting) continue; - loadPicture(entry.target); - observer.unobserve(entry.target); - } - }, { root: container.parentElement, rootMargin: '0px 800px' }) - : null; - - for (let loop = 0; loop < 2; loop++) { - for (const [index, image] of images.entries()) { - const picture = document.createElement('picture'); - const source = document.createElement('source'); - const img = document.createElement('img'); - const displayWidth = Math.round(290 * image.width / image.height); - picture.style.width = `${displayWidth}px`; - source.type = 'image/webp'; - source.sizes = `${displayWidth}px`; - source.dataset.srcset = image.variants.map(function(variant) { - return `${variant.url} ${variant.width}w`; - }).join(', '); - img.dataset.src = image.fallback; - img.width = image.width; - img.height = image.height; - img.alt = `Parnaíba - Foto ${index + 1}`; - img.decoding = 'async'; - img.loading = observer ? 'eager' : 'lazy'; - picture.append(source, img); - container.appendChild(picture); - pictures.push(picture); - if (!observer) { - loadPicture(picture); - } - } - } - - if (observer) { - const galleryObserver = new IntersectionObserver(function(entries) { - for (const entry of entries) { - container.classList.toggle('is-visible', entry.isIntersecting); - if (entry.isIntersecting) { - for (const picture of pictures) { - if (!picture.querySelector('img').hasAttribute('src')) { - observer.observe(picture); - } - } - } else { - observer.disconnect(); - } - } - }, { rootMargin: '400px 0px' }); - galleryObserver.observe(container.parentElement); - } else { - container.classList.add('is-visible'); - } -}); \ No newline at end of file diff --git a/tests/test_images.mjs b/tests/test_images.mjs deleted file mode 100644 index e72108f..0000000 --- a/tests/test_images.mjs +++ /dev/null @@ -1,119 +0,0 @@ -import assert from 'node:assert/strict'; -import { createHash } from 'node:crypto'; -import { readFileSync, existsSync } from 'node:fs'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import test from 'node:test'; -import { runInNewContext } from 'node:vm'; - -const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); -const images = JSON.parse(readFileSync(resolve(root, 'static/img/background/manifest.json'))); -const script = readFileSync(resolve(root, 'static/js/phb_photos_animation.js'), 'utf8'); - -test('responsive assets exist and use content-versioned WebP URLs', () => { - assert.equal(images.length, 23); - for (const [index, image] of images.entries()) { - assert.ok(image.width > 0 && image.height > 0); - assert.equal(image.fallback, `static/img/background/${String(index).padStart(2, '0')}.jpg`); - assert.ok(existsSync(resolve(root, image.fallback))); - assert.deepEqual(image.variants.map(variant => variant.width), [480, 800]); - for (const variant of image.variants) { - const bytes = readFileSync(resolve(root, variant.url)); - assert.equal(bytes.toString('ascii', 0, 4), 'RIFF'); - assert.equal(bytes.toString('ascii', 8, 12), 'WEBP'); - const hash = createHash('sha256').update(bytes).digest('hex').slice(0, 12); - assert.ok(variant.url.endsWith(`-${variant.width}.${hash}.webp`)); - } - } -}); - -async function renderCarousel(withObserver = true) { - const observers = []; - const createElement = function(tag) { - return { - tag, children: [], dataset: {}, style: {}, - append(...children) { this.children.push(...children); }, - appendChild(child) { this.children.push(child); }, - querySelector(selector) { return this.children.find(child => child.tag === selector); }, - hasAttribute(name) { return Object.hasOwn(this, name); }, - classList: { - classes: new Set(), - add(name) { this.classes.add(name); }, - toggle(name, enabled) { - if (enabled) this.classes.add(name); - else this.classes.delete(name); - }, - }, - }; - }; - const container = createElement('div'); - container.parentElement = createElement('div'); - let ready; - class MockObserver { - constructor(callback, options) { - this.callback = callback; - this.options = options; - this.targets = new Set(); - observers.push(this); - } - observe(target) { this.targets.add(target); } - unobserve(target) { this.targets.delete(target); } - disconnect() { this.targets.clear(); } - } - const context = { - document: { - querySelector: () => container, - createElement, - addEventListener: (event, callback) => { ready = callback; }, - }, - fetch: async () => ({ ok: true, json: async () => images }), - window: withObserver ? { IntersectionObserver: MockObserver } : {}, - IntersectionObserver: MockObserver, - }; - runInNewContext(script, context); - await ready(); - return { container, observers }; -} - -test('carousel reserves dimensions and defers downloads until near the gallery', async () => { - const { container, observers } = await renderCarousel(); - assert.equal(container.children.length, 46); - const [photoObserver, galleryObserver] = observers; - assert.equal(photoObserver.options.root, container.parentElement); - assert.equal(photoObserver.options.rootMargin, '0px 800px'); - assert.equal(galleryObserver.options.rootMargin, '400px 0px'); - assert.equal(photoObserver.targets.size, 0); - for (const picture of container.children) { - const img = picture.querySelector('img'); - assert.ok(img.width > 0 && img.height > 0); - assert.match(picture.style.width, /^\d+px$/); - assert.equal(img.src, undefined); - assert.equal(picture.querySelector('source').srcset, undefined); - } - galleryObserver.callback([{ isIntersecting: true }]); - assert.equal(photoObserver.targets.size, 46); - assert.ok(container.classList.classes.has('is-visible')); - const first = container.children[0]; - photoObserver.callback([{ target: first, isIntersecting: false }]); - assert.equal(first.querySelector('img').src, undefined); - photoObserver.callback([{ target: first, isIntersecting: true }]); - assert.equal(first.querySelector('img').src, images[0].fallback); - assert.match(first.querySelector('source').srcset, /480w, .*800w/); - assert.equal(photoObserver.targets.size, 45); - galleryObserver.callback([{ isIntersecting: false }]); - assert.equal(photoObserver.targets.size, 0); - assert.ok(!container.classList.classes.has('is-visible')); - galleryObserver.callback([{ isIntersecting: true }]); - assert.equal(photoObserver.targets.size, 45); -}); - -test('without IntersectionObserver, native lazy loading remains available', async () => { - const { container, observers } = await renderCarousel(false); - assert.equal(observers.length, 0); - assert.ok(container.classList.classes.has('is-visible')); - for (const picture of container.children) { - assert.equal(picture.querySelector('img').loading, 'lazy'); - assert.ok(picture.querySelector('img').src); - assert.ok(picture.querySelector('source').srcset); - } -}); \ No newline at end of file diff --git a/tests/test_site.py b/tests/test_site.py index d1c2136..a99bab1 100644 --- a/tests/test_site.py +++ b/tests/test_site.py @@ -3,7 +3,13 @@ import pytest import main -from main import load_settings, output_folder, render, settings_file +from main import ( + load_destination_slides, + load_settings, + output_folder, + render, + settings_file, +) def test_variables_yaml_exists_and_valid(): @@ -163,3 +169,72 @@ def test_meta_description_used_in_search_and_social_tags(): f'name="twitter:description" content="{meta_description}"', ): assert tag in index_html, f"Meta tag ausente ou divergente: {tag}" + + +def test_event_date_and_venue_rendered_in_hero(): + settings = load_settings() + event = settings["event"] + location = event["location"] + render() + index_html = (output_folder / "index.html").read_text(encoding="utf-8") + + assert ( + f'' + in index_html + ) + assert ( + f"{location['venue']}, em {location['city']}, {location['state_code']}" + in index_html + ) + + +def test_destination_slides_reference_existing_photos(): + settings = load_settings() + slides = load_destination_slides(settings) + assert slides, "destination.highlights deve ter ao menos uma atração." + + for slide in slides: + assert slide.get("title") and slide.get("description") and slide.get("alt") + assert slide["image"].endswith(".jpg") + assert Path(slide["image"]).exists() + + +def test_destination_carousel_rendered_with_captions(): + slides = load_destination_slides(load_settings()) + render() + index_html = (output_folder / "index.html").read_text(encoding="utf-8") + + assert index_html.count('class="carousel-slide"') == len(slides) + assert index_html.count("data-carousel-dot=") == len(slides) + for slide in slides: + assert f'

{slide["title"]}

' in index_html + assert f'Campus Tecnologia" # Descrição curta usada nas meta tags (Google, og:description, twitter:description). # Recomendado: até ~160 caracteres para não ser cortada nos resultados de busca. - meta_description: "Desde 2013, a Python Nordeste inclui pessoas na tecnologia e dissemina Python no Nordeste. Em 2027, o evento acontece em Parnaíba-PI." - date: "2027" # Pode ser "15 a 18 de Outubro" ou "Em breve" + meta_description: "Desde 2013, a Python Nordeste inclui pessoas na tecnologia e dissemina Python no Nordeste. Em 2027: de 13 a 15 de agosto, na UFDPar, em Parnaíba-PI." + date: "13, 14 e 15 de agosto de 2027" # Texto exibido no site. Pode ser "Em breve" + start_date: "2027-08-13" # Formato AAAA-MM-DD (opcional, usado no