feat: improvements
This commit is contained in:
@@ -1 +0,0 @@
|
||||
VITE_SITE_URL=https://crr.jcarreira.pt
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 143 KiB |
@@ -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
@@ -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 |
@@ -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
@@ -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>
|
||||
|
||||
@@ -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 +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 |
@@ -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 |
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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; }
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user