feat(filters): use categoria (closed set, 13 values) instead of temas for 'Tema' filter
ci / build-and-test (push) Canceled after 0s

The Tema dropdown previously listed every unique value from the IA-generated
temas array, producing 100+ pills with duplicates like 'desarrollo sostenible'
vs 'desarrollo_sostenible', 'biodiversidad' vs 'biodiversidad_marina', etc.

Switch the Tema filter source from aviso.temas to the new aviso.categoria
column (13 fixed values). The descriptive temas tags continue to render
inside each card unchanged.

- types.ts: add categoria: string | null to Aviso
- db.ts: SELECT categoria in both queries, map to Aviso
- index.astro: compute categorias from aviso.categoria, pass to Filters
- Filters.astro: rename prop to categorias, switch data-filter-type to
  'categoria', read data-categoria (single value, exact match) instead of
  data-temas (pipe-split, includes)
- EntryCard.astro: emit data-categoria instead of data-temas

The temas array is still rendered inside each card via aviso.temas.map().
This commit is contained in:
2026-07-27 10:06:47 +02:00
parent a06316dade
commit 4013cb8365
5 changed files with 19 additions and 16 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ const fecha = new Date(aviso.fecha).toLocaleDateString('ca-ES', {
class="entry"
aria-labelledby={titleId}
data-municipio={aviso.municipio ?? ''}
data-temas={aviso.temas.join('|')}
data-categoria={aviso.categoria ?? ''}
data-fuente={aviso.fuente}
>
{
+9 -9
View File
@@ -1,11 +1,11 @@
---
interface Props {
municipios: string[];
temas: string[];
categorias: string[];
fuentes: string[];
total: number;
}
const { municipios, temas, fuentes, total } = Astro.props;
const { municipios, categorias, fuentes, total } = Astro.props;
const initialCount = `${total} avís${total === 1 ? '' : 'os'} ambiental${total === 1 ? '' : 's'}`;
---
@@ -22,9 +22,9 @@ const initialCount = `${total} avís${total === 1 ? '' : 'os'} ambiental${total
<fieldset class="filters__group">
<legend class="filters__label">Tema</legend>
<div class="filters__pills" role="group" aria-label="Filtra per tema" data-filter-type="tema">
<div class="filters__pills" role="group" aria-label="Filtra per tema" data-filter-type="categoria">
<button type="button" class="pill is-active" aria-pressed="true" aria-controls="feed-list" data-value="">Tots</button>
{temas.map((t) => <button type="button" class="pill" aria-pressed="false" aria-controls="feed-list" data-value={t}>{t}</button>)}
{categorias.map((c) => <button type="button" class="pill" aria-pressed="false" aria-controls="feed-list" data-value={c}>{c}</button>)}
</div>
</fieldset>
@@ -115,9 +115,9 @@ const initialCount = `${total} avís${total === 1 ? '' : 'os'} ambiental${total
<script>
// Filtrado puro en el DOM: los avisos ya están renderizados en HTML
// estático (build-time). No hay llamadas a red al cambiar un filtro.
type FilterState = { municipio: string; tema: string; fuente: string };
type FilterState = { municipio: string; categoria: string; fuente: string };
const state: FilterState = { municipio: '', tema: '', fuente: '' };
const state: FilterState = { municipio: '', categoria: '', fuente: '' };
const root = document.getElementById('filters');
const countEl = document.getElementById('filters-count');
const entries = Array.from(document.querySelectorAll<HTMLElement>('.entry'));
@@ -126,12 +126,12 @@ const initialCount = `${total} avís${total === 1 ? '' : 'os'} ambiental${total
let visible = 0;
for (const entry of entries) {
const municipio = entry.dataset.municipio ?? '';
const temas = (entry.dataset.temas ?? '').split('|');
const categoria = entry.dataset.categoria ?? '';
const fuente = entry.dataset.fuente ?? '';
const matches =
(!state.municipio || municipio === state.municipio) &&
(!state.tema || temas.includes(state.tema)) &&
(!state.categoria || categoria === state.categoria) &&
(!state.fuente || fuente === state.fuente);
entry.hidden = !matches;
@@ -160,4 +160,4 @@ const initialCount = `${total} avís${total === 1 ? '' : 'os'} ambiental${total
});
apply();
</script>
</script>
+4 -2
View File
@@ -30,14 +30,14 @@ export async function getAvisosAmbientales(): Promise<Aviso[]> {
const [boib, prensa] = await Promise.all([
pool.query(`
select id, boib_fecha as fecha, organismo as fuente, titulo,
resumen, temas, url_html as url, numero_edicte
resumen, temas, categoria, url_html as url, numero_edicte
from boib_entradas
where es_ambiental = true
order by boib_fecha desc
`),
pool.query(`
select id, coalesce(fecha_publicacion, fecha_ingesta) as fecha,
fuente, titulo, resumen_ia as resumen, temas, url,
fuente, titulo, resumen_ia as resumen, temas, categoria, url,
case when municipio = 'Ibiza' then 'Eivissa' else municipio end as municipio
from noticias_prensa
where es_ambiental = true
@@ -51,6 +51,7 @@ export async function getAvisosAmbientales(): Promise<Aviso[]> {
titulo: r.titulo,
resumen: r.resumen ?? '',
temas: r.temas ?? [],
categoria: r.categoria ?? null,
fuente: r.fuente ?? 'BOIB',
municipio: null, // el BOIB no trae municipio estructurado, no forzar
url: r.url,
@@ -64,6 +65,7 @@ export async function getAvisosAmbientales(): Promise<Aviso[]> {
titulo: r.titulo,
resumen: r.resumen ?? '',
temas: r.temas ?? [],
categoria: r.categoria ?? null,
fuente: r.fuente,
municipio: r.municipio ?? null,
url: r.url,
+2 -1
View File
@@ -6,7 +6,8 @@ export interface Aviso {
origen: Origen;
titulo: string;
resumen: string; // resumen (BOIB) o resumen_ia (prensa) — nunca el cuerpo original
temas: string[];
temas: string[]; // etiquetas descriptivas libres (IA), se muestran dentro de la tarjeta
categoria: string | null; // categoría cerrada (13 valores fijos), alimenta el filtro "Tema"
fuente: string; // organismo (BOIB) o fuente (prensa)
municipio: string | null;
url: string;
+3 -3
View File
@@ -8,7 +8,7 @@ import { getAvisosAmbientales } from '../lib/db';
const avisos = await getAvisosAmbientales();
const municipios = [...new Set(avisos.map((a) => a.municipio).filter(Boolean))].sort() as string[];
const temas = [...new Set(avisos.flatMap((a) => a.temas))].sort();
const categorias = [...new Set(avisos.map((a) => a.categoria).filter(Boolean))].sort() as string[];
const fuentes = [...new Set(avisos.map((a) => a.fuente))].sort();
const avisosPorMunicipio = new Map<string, number>();
for (const aviso of avisos) {
@@ -59,7 +59,7 @@ const itemListSchema = {
<MapView avisos={avisos} descriptionId="map-description" client:media="(min-width: 900px)" />
</section>
<section class="map-alternative" aria-labelledby="map-alternative-title">
<section class="map-alternative" aria-labelledby="map-alternative-title">
<h2 id="map-alternative-title">Resum d'avisos per municipi</h2>
<p>El resum textual ofereix la mateixa informació de distribució que el mapa.</p>
<ul>
@@ -70,7 +70,7 @@ const itemListSchema = {
</section>
<section class="feed" aria-labelledby="page-title">
<Filters municipios={municipios} temas={temas} fuentes={fuentes} total={avisos.length} />
<Filters municipios={municipios} categorias={categorias} fuentes={fuentes} total={avisos.length} />
{avisos.length === 0 && (
<p class="empty">No hi ha avisos ambientals classificats encara. Torna més tard.</p>