-
+ {% if destination_slides %}
+
+
+
+ {% for slide in destination_slides %}
+
+
+
+
+ {{ '%02d' % loop.index }} / {{ '%02d' % loop.length }}
+ {{ slide.title }}
+ {{ slide.description }}
+
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+ {% for slide in destination_slides %}
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+ {% endif %}
+
+
+
+ {% set location = event.location or {} %}
+ {% if location.venue %}
+
+
+
+
+
+ {% if location.photo %}
+
+ {% 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'
{event["date"]} '
+ 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'
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
para buscadores)
+ end_date: "2027-08-15" # Formato AAAA-MM-DD (opcional)
format: "presencial" # "presencial" ou "online"
url: "https://2027.pythonnordeste.org/"
location:
city: "Parnaíba"
state: "Piauí"
- venue: "" # Opcional
- map_url: "" # Opcional
+ state_code: "PI"
+ venue: "UFDPar" # Opcional
+ venue_full_name: "Universidade Federal do Delta do Parnaíba" # Opcional
+ map_url: "https://www.google.com/maps/search/?api=1&query=UFDPar+Parna%C3%ADba+PI" # Opcional
+ address: "Av. São Sebastião, 2819 - Nossa Senhora de Fátima" # Opcional
+ # Busca usada no mapa incorporado da seção "Local do evento" (opcional).
+ map_query: "UFDPar - Universidade Federal do Delta do Parnaíba, Parnaíba - PI"
+ # Foto do local (opcional). Enquanto estiver vazio, a seção mostra um espaço reservado.
+ photo: "static/img/ufdpar.jpg"
+ photo_alt: "Totem de entrada da UFDPar com o nome e o brasão da universidade, sob céu azul" # Descrição da foto para leitores de tela
# Identidade Visual e Mídia
theme:
@@ -32,6 +42,68 @@ theme:
preview_image_width: 1920
preview_image_height: 1080
+# Carrossel "Parnaíba espera por você": atrações da cidade e da região.
+# "photo" é o número da foto em static/img/city/ (ex.: "01" -> 01.jpg).
+# "alt" descreve a imagem para leitores de tela; "description" aparece na legenda.
+destination:
+ highlights:
+ - photo: "01"
+ title: "Delta do Parnaíba"
+ description: "Único delta em mar aberto das Américas, com dezenas de ilhas, igarapés e dunas entre o Piauí e o Maranhão. O passeio de barco é o programa obrigatório de quem visita a cidade."
+ alt: "Vista aérea de um barco navegando por um rio largo cercado de manguezais, com o mar ao fundo"
+ - photo: "21"
+ title: "Revoada dos guarás"
+ description: "No fim da tarde, bandos de guarás, aves de plumagem vermelha intensa, cruzam o céu do Delta de volta aos manguezais. Um dos espetáculos mais marcantes da região."
+ alt: "Bando de guarás vermelhos voando sobre a água, com dunas ao fundo"
+ - photo: "10"
+ title: "Manguezais e igarapés"
+ description: "Os passeios atravessam canais estreitos entre raízes de mangue, berçário de caranguejos, peixes e aves. Dá para conhecer de perto o ecossistema que sustenta o Delta."
+ alt: "Raízes aéreas entrelaçadas de um manguezal sobre a água"
+ - photo: "12"
+ title: "Dunas e lagoas"
+ description: "Dunas brancas que se encontram com o mar e formam lagoas de água doce na estação chuvosa. A paisagem continua até os Lençóis Maranhenses, na Rota das Emoções."
+ alt: "Vista aérea de uma faixa de dunas brancas entre o mar e lagoas esverdeadas"
+ - photo: "05"
+ title: "Passeios pelas dunas"
+ description: "De buggy, 4x4 ou a pé, os passeios levam a mirantes onde a areia termina direto na água, com vista para o rio, o mar e as ilhas do Delta."
+ alt: "Duna de areia clara terminando em água azul, com um veículo de passeio no alto"
+ - photo: "16"
+ title: "Porto das Barcas"
+ description: "Antigo porto às margens do Rio Igaraçu, hoje o coração cultural de Parnaíba: casarões históricos com restaurantes, bares, lojas de artesanato e o pôr do sol à beira do rio."
+ alt: "Casarão histórico com torre laranja à margem do rio ao entardecer"
+ - photo: "07"
+ title: "Centro histórico"
+ description: "Casarões e fachadas ornamentadas contam a história de Parnaíba, que foi um importante entreposto comercial do Nordeste. Vale caminhar sem pressa pelas ruas do centro."
+ alt: "Fachada histórica com três portas de madeira emolduradas por ornamentos em relevo"
+ - photo: "03"
+ title: "Parnaíba à noite"
+ description: "As torres da Catedral de Nossa Senhora da Graça despontam no centro da cidade, que tem vida noturna animada e boa comida regional, do caranguejo ao peixe fresco."
+ alt: "Vista noturna da cidade com as torres de uma igreja iluminadas em primeiro plano"
+ - photo: "00"
+ title: "Artesanato local"
+ description: "Peças em madeira, palha de carnaúba, cerâmica e renda traduzem a criatividade de artesãs e artesãos da região. Ótimas lembranças para levar para casa."
+ alt: "Esculturas de madeira representando figuras populares carregando cestos"
+ - photo: "20"
+ title: "Quadrilhódromo"
+ description: "Palco das quadrilhas juninas de Parnaíba, com murais que celebram o bumba-meu-boi e a cultura popular. Na época do São João, o espaço reúne grupos, forró e comidas típicas, e o resto do ano a praça segue como ponto de encontro da cidade."
+ alt: "Vista aérea noturna do Quadrilhódromo, com a praça movimentada e o galpão decorado por murais coloridos"
+ - photo: "19"
+ title: "Praia da Pedra do Sal"
+ description: "Um farol sobre as rochas separa dois mares: de um lado, águas calmas; do outro, ondas que atraem surfistas e praticantes de kitesurf."
+ alt: "Vista aérea de uma ponta rochosa com farol entre duas praias, uma de mar calmo e outra de mar agitado"
+ - photo: "18"
+ title: "Litoral do Piauí"
+ description: "Perto de Parnaíba ficam praias como Atalaia, Coqueiro e Barra Grande, com ventos constantes, lagoas e pores do sol inesquecíveis."
+ alt: "Pôr do sol dourado sob um céu cheio de nuvens em uma praia extensa"
+ - photo: "15"
+ title: "Carnaubais"
+ description: "A carnaúba, conhecida como árvore da vida, está por toda parte na paisagem do Piauí. Dela vêm a cera, a palha do artesanato e a madeira das construções tradicionais."
+ alt: "Silhueta de carnaúbas contra o sol se pondo em um céu alaranjado"
+ - photo: "22"
+ title: "Entardecer no Delta"
+ description: "O dia termina com o céu colorido e as aves voltando para as árvores. Bem-vinda e bem-vindo a Parnaíba: esperamos você na Python Nordeste 2027!"
+ alt: "Silhueta de árvores do mangue e um bando de aves voando no céu do entardecer"
+
# Links e Ações Principais (habilitados conforme a fase do evento)
actions:
tickets_url: "" # Link do Sympla / Eventbrite (se vazio, esconde)