feat(filters): use categoria (closed set, 13 values) instead of temas for 'Tema' filter
ci / build-and-test (push) Canceled after 0s
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:
@@ -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}
|
||||
>
|
||||
{
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user