feat: improvements

This commit is contained in:
2026-09-01 09:56:10 +01:00
parent 74386d26a2
commit a043f647bd
22 changed files with 217 additions and 141 deletions
-1
View File
@@ -1 +0,0 @@
VITE_SITE_URL=https://crr.jcarreira.pt
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

+5 -5
View File
@@ -63,13 +63,13 @@ The final image contains nginx and static files only. Node.js is used only in th
## Managing events
Each event is one Markdown file in `src/content/events/`. Its YAML frontmatter is the source of structured data and its Markdown body is the event information shown on the page.
Each event is one Markdown file in `content/events/`, separate from the Vue application source. Its YAML frontmatter is the source of structured data and its Markdown body is the event information shown on the page.
To add an event:
1. Copy `src/content/events/festas-2026.md` to a new kebab-case filename.
2. Update the title, slug, dates, poster path, Facebook post URL, and suggested text.
3. Add the poster to `public/images/events/`.
1. Copy `content/events/festas-2026.md` to a new kebab-case filename.
2. Update the title, slug, dates, poster filename, Facebook post URL, and suggested text.
3. Add the poster image next to that event's Markdown file in `content/events/`.
4. Set exactly one event to `active: true`; mark the previous event `false`.
5. Run `npm run validate` and rebuild the Docker image.
@@ -90,7 +90,7 @@ date:
location:
name: "Centro Recreativo da Rebolaria"
locality: "Rebolaria, Batalha"
poster: "/images/events/nome-do-evento-2027.jpg"
poster: "nome-do-evento-2027.jpg"
facebook:
url: "https://www.facebook.com/..."
embed: true
+2 -2
View File
@@ -3,8 +3,8 @@ services:
build:
context: .
args:
VITE_SITE_URL: ${VITE_SITE_URL:-https://crr.jcarreira.pt}
VITE_SITE_URL: "https://crr.jcarreira.pt"
container_name: crr-share
restart: unless-stopped
ports:
- "8080:80"
- "8200:80"
Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

+50
View File
@@ -0,0 +1,50 @@
---
title: "Festas do 66.º Aniversário"
slug: "festas-2026"
active: true
date:
start: "2026-09-05"
end: "2026-09-07"
location:
name: "Centro Recreativo da Rebolaria"
locality: "Batalha"
poster: "festas-2026.jpeg"
facebook:
url: "https://www.facebook.com/story.php?story_fbid=122327514458210862&id=61556325866671"
embed: true
share:
text: |
Já está tudo preparado para mais um grande fim de semana na Rebolaria! 🎉🥳
Dias 5, 6 e 7 de setembro, apareçam e venham festejar connosco! ❤️💚
Vejam o programa completo aqui em baixo 👇
---
## Sobre o evento
Nos dias **5, 6 e 7 de setembro de 2026**, o **Centro Recreativo da Rebolaria** celebra o seu **66.º aniversário** com três dias de festa, música, tradição e convívio.
As comemorações contam com um programa pensado para todas as idades, incluindo o **Torneio de Penalty Cup**, jogos tradicionais, folclore, animação musical e serviço de restaurante. Ao longo dos três dias, o palco recebe vários artistas e grupos, com destaque para **Pedro Nobre**, **DJ Pinona**, a banda **Red Kiss**, o **Rancho Folclórico Rosas do Lena** e **Fábio Tacho**.
### Sábado, 5 de setembro
- **15h00** — Torneio de Penalty Cup
- **19h00** — Abertura do restaurante
- **22h00** — Atuação de **Pedro Nobre**
- Segue-se **DJ Pinona**
### Domingo, 6 de setembro
- **12h00** — Abertura do arraial
- **16h30** — Atuação do **Rancho Folclórico Rosas do Lena**
- **19h00** — Abertura do restaurante
- **22h00** — Atuação da banda **Red Kiss**
### Segunda-feira, 7 de setembro
- **18h30** — Jogos tradicionais
- **19h00** — Abertura do restaurante
- **22h00** — Atuação de **Fábio Tacho**
Uma celebração especial dos **66 anos de história do Centro Recreativo da Rebolaria**, reunindo associados, famílias, amigos e toda a comunidade num ambiente de festa e tradição.
+1 -2
View File
@@ -3,8 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#155e3a" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<meta name="theme-color" content="#f31928" />
</head>
<body>
<div id="app"></div>
+1
View File
@@ -6,6 +6,7 @@ server {
root /usr/share/nginx/html;
index index.html;
charset utf-8;
absolute_redirect off;
gzip on;
gzip_vary on;
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#155e3a"/><path d="M14 45V19h16c11 0 18 5 18 13 0 9-7 13-18 13h-7v-9h7c5 0 8-1 8-4s-3-4-8-4h-7v17Z" fill="#f8eedc"/><circle cx="48" cy="46" r="6" fill="#a41e34"/></svg>

Before

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

-17
View File
@@ -1,17 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1080 1350" role="img" aria-labelledby="title desc">
<title id="title">Festas do 66.º Aniversário do Centro Recreativo da Rebolaria</title>
<desc id="desc">Cartaz informativo para as festas de 5 a 7 de setembro de 2026.</desc>
<rect width="1080" height="1350" fill="#f8eedc"/>
<path d="M0 0h1080v286c-244-83-439-61-648 11C268 355 130 354 0 300Z" fill="#155e3a"/>
<path d="M0 1090c263-61 479-41 670 70 174 101 290 99 410 59v131H0Z" fill="#a41e34"/>
<g fill="#d4a62a"><circle cx="125" cy="184" r="13"/><circle cx="195" cy="122" r="8"/><circle cx="868" cy="132" r="13"/><circle cx="946" cy="200" r="8"/></g>
<text x="540" y="150" fill="#fff" font-family="Georgia, serif" font-size="35" font-weight="700" text-anchor="middle" letter-spacing="4">CENTRO RECREATIVO DA REBOLARIA</text>
<text x="540" y="463" fill="#155e3a" font-family="Georgia, serif" font-size="94" font-weight="700" text-anchor="middle">FESTAS DO</text>
<text x="540" y="575" fill="#a41e34" font-family="Georgia, serif" font-size="124" font-weight="700" text-anchor="middle">66.º</text>
<text x="540" y="652" fill="#155e3a" font-family="Georgia, serif" font-size="68" font-weight="700" text-anchor="middle">ANIVERSÁRIO</text>
<line x1="265" x2="815" y1="706" y2="706" stroke="#d4a62a" stroke-width="8"/>
<text x="540" y="790" fill="#155e3a" font-family="Arial, sans-serif" font-size="53" font-weight="700" text-anchor="middle">5 · 6 · 7 SETEMBRO 2026</text>
<text x="540" y="862" fill="#5f3c24" font-family="Arial, sans-serif" font-size="35" text-anchor="middle">MÚSICA · TRADIÇÃO · CONVÍVIO</text>
<text x="540" y="930" fill="#5f3c24" font-family="Arial, sans-serif" font-size="30" text-anchor="middle">REBOLARIA, BATALHA</text>
<text x="540" y="1260" fill="#fff" font-family="Arial, sans-serif" font-size="34" font-weight="700" text-anchor="middle">ENTRADA LIVRE</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

+4 -5
View File
@@ -3,8 +3,7 @@ import { resolve } from 'node:path';
import { parseEventSource } from '../src/data/event-parser';
import { validateEventCollection } from '../src/data/event-schema';
const eventDirectory = resolve(process.cwd(), 'src/content/events');
const publicDirectory = resolve(process.cwd(), 'public');
const eventDirectory = resolve(process.cwd(), 'content/events');
const eventFiles = readdirSync(eventDirectory).filter((file) => file.endsWith('.md')).sort();
if (eventFiles.length === 0) {
@@ -14,10 +13,10 @@ if (eventFiles.length === 0) {
const events = eventFiles.map((file) => {
const filePath = resolve(eventDirectory, file);
const event = parseEventSource(readFileSync(filePath, 'utf8'), file);
const posterPath = resolve(publicDirectory, `.${event.poster}`);
const posterPath = resolve(eventDirectory, event.poster);
if (!posterPath.startsWith(`${publicDirectory}/`) || !existsSync(posterPath)) {
throw new Error(`${file}: poster does not exist in public/: ${event.poster}`);
if (!posterPath.startsWith(`${eventDirectory}/`) || !existsSync(posterPath)) {
throw new Error(`${file}: poster does not exist next to the event Markdown file: ${event.poster}`);
}
return event;
+6
View File
@@ -1,6 +1,12 @@
<script setup lang="ts">
import { useHead } from '@unhead/vue';
import { RouterView } from 'vue-router';
import DefaultLayout from '@/layouts/DefaultLayout.vue';
import logoUrl from '../LogoCRR.png';
useHead({
link: [{ rel: 'icon', type: 'image/png', href: logoUrl }],
});
</script>
<template>
+6 -2
View File
@@ -1,8 +1,12 @@
<script setup lang="ts">
import logoUrl from '../../LogoCRR.png';
</script>
<template>
<header class="site-header">
<RouterLink class="brand" to="/" aria-label="Centro Recreativo da Rebolaria, página inicial">
<span class="brand-mark" aria-hidden="true">CRR</span>
<span>
<img class="brand-logo" :src="logoUrl" alt="" />
<span class="brand-copy">
<strong>Centro Recreativo</strong>
<span>da Rebolaria</span>
</span>
+82 -25
View File
@@ -6,34 +6,76 @@ const props = defineProps<{
facebookUrl: string;
}>();
const textArea = ref<HTMLTextAreaElement>();
const status = ref('');
const copied = ref(false);
const canNativeShare = ref(false);
let resetStatusTimer: number | undefined;
const textArea = ref<HTMLTextAreaElement>();
let statusTimer: number | undefined;
function setStatus(message: string, clearAfter = 0) {
status.value = message;
if (resetStatusTimer) window.clearTimeout(resetStatusTimer);
if (clearAfter) resetStatusTimer = window.setTimeout(() => (status.value = ''), clearAfter);
function showCopiedStatus() {
copied.value = true;
status.value = 'Texto copiado para a área de transferência.';
if (statusTimer) window.clearTimeout(statusTimer);
statusTimer = window.setTimeout(() => {
copied.value = false;
status.value = '';
}, 2500);
}
async function copyText() {
function copyWithSelection(): boolean {
let copiedSuccessfully = false;
const handleCopy = (event: ClipboardEvent) => {
if (!event.clipboardData) return;
event.preventDefault();
event.clipboardData.setData('text/plain', props.shareText);
copiedSuccessfully = true;
};
selectVisibleText();
document.addEventListener('copy', handleCopy);
try {
if (!navigator.clipboard) throw new Error('Clipboard API unavailable');
await navigator.clipboard.writeText(props.shareText);
setStatus('Texto copiado.', 2500);
return;
document.execCommand('copy');
} catch {
textArea.value?.focus();
textArea.value?.select();
const copied = document.execCommand('copy');
setStatus(
copied
? 'Texto copiado.'
: 'Não foi possível copiar automaticamente. Selecione o texto acima e copie-o manualmente.',
copied ? 2500 : 0,
);
copiedSuccessfully = false;
} finally {
document.removeEventListener('copy', handleCopy);
}
return copiedSuccessfully;
}
function selectVisibleText() {
const element = textArea.value;
if (!element) return;
element.focus();
element.select();
element.setSelectionRange(0, element.value.length);
}
function copyText() {
status.value = '';
const clipboardRequest = window.isSecureContext && navigator.clipboard?.writeText
? navigator.clipboard.writeText(props.shareText)
: undefined;
const copiedWithSelection = copyWithSelection();
if (copiedWithSelection) {
showCopiedStatus();
void clipboardRequest?.catch(() => undefined);
return;
}
if (clipboardRequest) {
void clipboardRequest.then(showCopiedStatus).catch(() => {
selectVisibleText();
status.value = 'Não foi possível copiar automaticamente. O texto ficou selecionado para copiares manualmente.';
});
return;
}
selectVisibleText();
status.value = 'Não foi possível copiar automaticamente. O texto ficou selecionado para copiares manualmente.';
}
async function sharePage() {
@@ -41,7 +83,7 @@ async function sharePage() {
await navigator.share({ title: document.title, url: window.location.href });
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') return;
setStatus('Não foi possível partilhar esta página automaticamente.');
status.value = 'Não foi possível partilhar esta página automaticamente.';
}
}
@@ -50,7 +92,7 @@ onMounted(() => {
});
onBeforeUnmount(() => {
if (resetStatusTimer) window.clearTimeout(resetStatusTimer);
if (statusTimer) window.clearTimeout(statusTimer);
});
</script>
@@ -58,9 +100,24 @@ onBeforeUnmount(() => {
<section class="share-panel" aria-labelledby="share-title">
<p class="eyebrow">Ajuda-nos a divulgar</p>
<h2 id="share-title">Partilha este evento</h2>
<label class="sr-only" for="share-text">Texto sugerido para a publicação</label>
<textarea id="share-text" ref="textArea" class="share-text" :value="shareText" readonly rows="7" />
<button class="button button-copy" type="button" @click="copyText">Copiar texto</button>
<div class="share-text-card">
<div class="share-text-toolbar">
<label class="share-text-label" for="share-text">Texto sugerido</label>
<button
class="copy-button"
:class="{ 'is-copied': copied }"
type="button"
@click="copyText"
>
<svg class="copy-icon" viewBox="0 0 24 24" aria-hidden="true">
<path v-if="copied" d="m5 12 4 4L19 6" />
<path v-else d="M8 8V5a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-3M5 8h9a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2Z" />
</svg>
{{ copied ? 'Copiado' : 'Copiar texto' }}
</button>
</div>
<textarea id="share-text" ref="textArea" class="share-text" :value="shareText" readonly rows="7" />
</div>
<p class="copy-status" aria-live="polite">{{ status }}</p>
<a :href="facebookUrl" target="_blank" rel="noopener noreferrer" class="button button-facebook">
Abrir publicação no Facebook
-48
View File
@@ -1,48 +0,0 @@
---
title: "Festas do 66.º Aniversário"
slug: "festas-2026"
active: true
date:
start: "2026-09-05"
end: "2026-09-07"
location:
name: "Centro Recreativo da Rebolaria"
locality: "Rebolaria, Batalha"
poster: "/images/events/festas-2026.png"
facebook:
url: "https://www.facebook.com/story.php?story_fbid=122327514458210862&id=61556325866671"
embed: true
share:
text: |
Já está tudo preparado para mais um grande fim de semana na Rebolaria! 🎉🥳
Dias 5, 6 e 7 de setembro, apareçam e venham festejar connosco! ❤️💚
Vejam o programa completo aqui em baixo 👇
---
## Sobre o evento
O Centro Recreativo da Rebolaria celebra o seu **66.º aniversário** com três dias de música, tradição, convívio e animação.
## Programa
### Sábado, 5 de setembro
- 15h00 - Torneio de Penalty Cup
- 19h00 - Abertura do restaurante
- 22h00 - Pedro Nobre
- De seguida - DJ Pinona
### Domingo, 6 de setembro
- 12h00 - Abertura do arraial
- 16h30 - Rancho Folclórico Rosas do Lena
- 19h00 - Abertura do restaurante
- 22h00 - Red Kiss
### Segunda-feira, 7 de setembro
- 18h30 - Jogos tradicionais
- 19h00 - Abertura do restaurante
- 22h00 - Fábio Tacho - Teclista e Vocalista
+1 -1
View File
@@ -28,7 +28,7 @@ export const EventSchema = z
name: z.string().trim().min(1),
locality: z.string().trim().min(1).optional(),
}),
poster: z.string().startsWith('/', 'must be a root-relative public path'),
poster: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*\.(png|jpe?g|webp|avif)$/i, 'must be an image filename next to the event Markdown file'),
facebook: z.object({
url: z.string().url().refine(isFacebookUrl, 'must be an HTTPS facebook.com URL'),
embed: z.boolean(),
+18 -2
View File
@@ -4,14 +4,30 @@ import type { EventData, EventMetadata } from '@/types/event';
export { getEventMetadata, parseEventSource };
const files = import.meta.glob<string>('../content/events/*.md', {
const files = import.meta.glob<string>('../../content/events/*.md', {
query: '?raw',
import: 'default',
eager: true,
});
const posters = import.meta.glob<string>('../../content/events/*.{png,jpg,jpeg,webp,avif}', {
query: '?url',
import: 'default',
eager: true,
});
const events = Object.entries(files)
.map(([path, source]) => parseEventSource(source, path))
.map(([path, source]) => {
const event = parseEventSource(source, path);
const posterPath = path.replace(/[^/]+\.md$/, event.poster);
const poster = posters[posterPath];
if (!poster) {
throw new Error(`Poster for ${path} does not exist next to its Markdown file: ${event.poster}`);
}
return { ...event, poster };
})
.sort((left, right) => right.date.start.localeCompare(left.date.start));
validateEventCollection(events);
+1 -1
View File
@@ -36,8 +36,8 @@ useHead({
<article v-if="event" class="event-page">
<EventHeader :event="event" />
<EventPoster :src="event.poster" :title="event.title" />
<SharePanel :share-text="event.share.text" :facebook-url="event.facebook.url" />
<FacebookPost v-if="event.facebook.embed" :url="event.facebook.url" />
<SharePanel :share-text="event.share.text" :facebook-url="event.facebook.url" />
<section class="event-details" aria-label="Informação do evento" v-html="event.html" />
</article>
<section v-else class="empty-state" aria-labelledby="not-found-title">
-3
View File
@@ -20,14 +20,11 @@ useHead({
<template>
<section class="landing">
<p class="eyebrow">Centro Recreativo da Rebolaria</p>
<h1>Eventos em destaque</h1>
<article v-if="event" class="event-card">
<img :src="event.poster" :alt="`Cartaz de ${event.title}`" width="1080" height="1350" />
<div>
<h2>{{ event.title }}</h2>
<p>{{ event.location.name }}</p>
<RouterLink :to="`/partilhar/${event.slug}`" class="text-link">Ver evento</RouterLink>
<RouterLink to="/partilhar" class="button button-copy">Ajudar a divulgar</RouterLink>
</div>
</article>
+39 -25
View File
@@ -1,49 +1,64 @@
:root {
color: #1d2821;
background: #f4f0e8;
--crr-red: #f31928;
--crr-red-dark: #b70f1c;
--crr-red-deep: #771018;
--crr-rose: #fff3f4;
--crr-border: #efc9cc;
--crr-ink: #321216;
--crr-muted: #71555a;
color: var(--crr-ink);
background: var(--crr-rose);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-synthesis: none;
}
* { box-sizing: border-box; }
html { min-width: 320px; }
body { margin: 0; min-width: 320px; background: #f4f0e8; }
body { margin: 0; min-width: 320px; background: var(--crr-rose); }
button, textarea { font: inherit; }
a { color: #075b9b; }
a:focus-visible, button:focus-visible, textarea:focus-visible { outline: 3px solid #d79916; outline-offset: 3px; }
a { color: var(--crr-red-deep); }
a:focus-visible, button:focus-visible, textarea:focus-visible { outline: 3px solid var(--crr-red-dark); outline-offset: 3px; }
.site-shell { min-height: 100vh; display: flex; flex-direction: column; }
.site-header { background: #155e3a; color: #fff; }
.site-header { border-bottom: 4px solid var(--crr-red); background: #fff; color: var(--crr-ink); }
.brand, .page-content, .site-footer { width: min(100% - 32px, 680px); margin-inline: auto; }
.brand { min-height: 76px; display: flex; align-items: center; gap: 12px; color: #fff; text-decoration: none; }
.brand-mark { display: grid; place-items: center; width: 43px; height: 43px; border: 2px solid #efc660; border-radius: 50%; color: #efc660; font-weight: 800; font-size: .77rem; }
.brand strong, .brand span:last-child { display: block; }
.brand strong { font-size: 1rem; letter-spacing: .02em; }
.brand span:last-child { font-size: .84rem; }
.brand { min-height: 96px; display: flex; align-items: center; gap: 14px; color: var(--crr-ink); text-decoration: none; }
.brand-logo { width: 76px; height: 76px; flex: 0 0 auto; object-fit: contain; }
.brand-copy strong, .brand-copy span { display: block; }
.brand strong { color: var(--crr-red-deep); font-size: 1rem; letter-spacing: .02em; }
.brand-copy span { color: var(--crr-muted); font-size: .84rem; }
.page-content { flex: 1; padding-block: 34px 48px; }
.site-footer { padding: 24px 0 32px; color: #5c665f; font-size: .875rem; }
.site-footer { padding: 24px 0 32px; color: var(--crr-muted); font-size: .875rem; }
.site-footer p { margin: 0; }
.eyebrow { margin: 0 0 9px; color: #a41e34; font-size: .78rem; font-weight: 800; letter-spacing: .13em; text-transform: uppercase; }
h1, h2, h3 { color: #123c29; line-height: 1.12; }
.eyebrow { margin: 0 0 9px; color: var(--crr-red-dark); font-size: .78rem; font-weight: 800; letter-spacing: .13em; text-transform: uppercase; }
h1, h2, h3 { color: var(--crr-red-deep); line-height: 1.12; }
h1 { margin: 0; font-family: Georgia, serif; font-size: clamp(2rem, 8vw, 3.25rem); }
h2 { margin: 0; font-size: 1.45rem; }
h3 { font-size: 1.1rem; }
p, li { line-height: 1.6; }
.event-header { margin-bottom: 25px; }
.event-meta { margin: 7px 0 0; color: #4e584f; }
.event-poster { margin: 0 0 32px; overflow: hidden; border-radius: 4px; background: #e7e0d2; box-shadow: 0 13px 30px rgb(33 45 37 / 16%); }
.event-meta { margin: 7px 0 0; color: var(--crr-muted); }
.event-poster { margin: 0 0 32px; overflow: hidden; border-radius: 4px; background: #ffe6e8; box-shadow: 0 13px 30px rgb(119 16 24 / 16%); }
.event-poster img, .event-card img { display: block; width: 100%; height: auto; }
.share-panel, .facebook-post, .event-details, .event-card, .empty-state { border: 1px solid #ddd5c6; border-radius: 8px; background: #fffdf9; }
.share-panel, .facebook-post, .event-details, .event-card, .empty-state { border: 1px solid var(--crr-border); border-radius: 8px; background: #fff; }
.share-panel { padding: 25px; }
.share-panel h2 { margin-bottom: 18px; }
.share-text { display: block; width: 100%; min-height: 154px; padding: 14px; border: 1px solid #b9c3ba; border-radius: 4px; resize: vertical; background: #fff; color: #1d2821; line-height: 1.5; white-space: pre-wrap; }
.share-text-card { overflow: hidden; border: 1px solid var(--crr-border); border-radius: 12px; background: #fffafa; box-shadow: 0 8px 24px rgb(119 16 24 / 8%); }
.share-text-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 10px 10px 16px; border-bottom: 1px solid var(--crr-border); background: #fff3f4; }
.share-text-label { color: var(--crr-muted); font-size: .82rem; font-weight: 750; letter-spacing: .02em; }
.copy-button { display: inline-flex; min-height: 40px; align-items: center; justify-content: center; gap: 7px; border: 0; border-radius: 8px; padding: 8px 12px; background: var(--crr-red-dark); color: #fff; font-weight: 750; cursor: pointer; }
.copy-button:hover { background: var(--crr-red-deep); }
.copy-button.is-copied { background: #247346; }
.copy-icon { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 2; }
.share-text { display: block; width: 100%; min-height: 154px; padding: 18px; border: 0; resize: vertical; background: transparent; color: var(--crr-ink); font-size: 1rem; line-height: 1.6; white-space: pre-wrap; }
.share-text:focus { outline-offset: -3px; }
.button { display: flex; align-items: center; justify-content: center; width: 100%; min-height: 48px; margin-top: 14px; border: 0; border-radius: 4px; padding: 11px 16px; font-weight: 750; text-align: center; text-decoration: none; cursor: pointer; }
.button-copy { background: #155e3a; color: #fff; }
.button-facebook { background: #075b9b; color: #fff; }
.button-secondary { border: 1px solid #155e3a; background: transparent; color: #155e3a; }
.button-copy { background: var(--crr-red-dark); color: #fff; }
.button-facebook { background: var(--crr-red); color: #fff; }
.button-secondary { border: 1px solid var(--crr-red-dark); background: transparent; color: var(--crr-red-dark); }
.button:hover { filter: brightness(.94); }
.copy-status { min-height: 25px; margin: 8px 0 0; color: #155e3a; font-weight: 700; font-size: .9rem; }
.share-instructions { margin-top: 28px; border-top: 1px solid #e2dbcf; padding-top: 18px; }
.copy-status { min-height: 25px; margin: 8px 0 0; color: var(--crr-red-deep); font-weight: 700; font-size: .9rem; }
.share-instructions { margin-top: 28px; border-top: 1px solid var(--crr-border); padding-top: 18px; }
.share-instructions h3 { margin: 0 0 8px; }
.share-instructions ol { margin: 0; padding-left: 24px; }
.facebook-post { margin-top: 24px; padding: 25px; }
@@ -55,11 +70,10 @@ p, li { line-height: 1.6; }
.event-details h2 { margin-top: 34px; }
.event-details h3 { margin-top: 25px; }
.event-details li { margin: 6px 0; }
.landing > h1 { margin-bottom: 25px; }
.event-card { overflow: hidden; }
.event-card > div { padding: 23px; }
.event-card h2 { margin-bottom: 7px; }
.event-card p { margin: 0 0 18px; color: #4e584f; }
.event-card p { margin: 0 0 18px; color: var(--crr-muted); }
.text-link { display: inline-block; font-weight: 750; }
.event-card .button { margin-top: 18px; }
.empty-state { padding: 30px; }
+1 -1
View File
@@ -6,7 +6,7 @@ import { parseEventSource } from '../src/data/event-parser';
import { getStaticRoutes } from '../src/router/routes';
import { absoluteUrl } from '../src/config/site';
const festasSource = readFileSync(resolve(process.cwd(), 'src/content/events/festas-2026.md'), 'utf8');
const festasSource = readFileSync(resolve(process.cwd(), 'content/events/festas-2026.md'), 'utf8');
describe('event content', () => {
it('parses the initial event, preserving its share text and Markdown body', () => {