feat(web): afegir pagina /illes amb comparativa de les 4 illes

This commit is contained in:
2026-07-14 13:39:47 +02:00
parent 6b09a711fc
commit 8a3a44ed5f
4 changed files with 334 additions and 4 deletions
+10 -2
View File
@@ -4,7 +4,7 @@ import LanguageToggle from "./LanguageToggle.astro";
interface Props {
lang?: Lang;
active: "home" | "map" | "methodology";
active: "home" | "map" | "methodology" | "islands";
}
const { lang: langProp, active } = Astro.props;
@@ -13,8 +13,9 @@ const path = Astro.url.pathname;
const isHome = path === "/" || path === "";
const isMap = path.startsWith("/mapa");
const isMetod = path.startsWith("/metodologia");
const isIslands = path.startsWith("/illes");
const navLink = (key: "home" | "map" | "methodology") =>
const navLink = (key: "home" | "map" | "methodology" | "islands") =>
`text-sm font-medium px-3 py-1.5 rounded transition-colors ${
active === key
? "text-orange-700 bg-orange-50"
@@ -47,6 +48,13 @@ const navLink = (key: "home" | "map" | "methodology") =>
>
{t(lang, "nav.map")}
</a>
<a
href="/illes"
class={navLink("islands")}
aria-current={isIslands ? "page" : undefined}
>
{t(lang, "nav.islands")}
</a>
<a
href="/metodologia"
class={navLink("methodology")}
+55 -1
View File
@@ -8,7 +8,8 @@
"nav": {
"home": "Inici",
"map": "Mapa",
"methodology": "Metodologia"
"methodology": "Metodologia",
"islands": "Per illa"
},
"lang_toggle": {
"label": "Idioma",
@@ -122,5 +123,58 @@
"of_total": "del total",
"people": "persones",
"back_to_home": "Tornar a l'inici"
},
"islands": {
"title": "Per illa",
"lead": "Cada illa té una realitat distinta. Aquí les posem en paral·lel amb les mateixes dades, les mateixes fonts i les mateixes limitacions.",
"section_compare_title": "Comparativa ràpida",
"col_island": "Illa",
"col_sections": "Seccions",
"col_listings": "Listings Airbnb",
"col_entire": "Pisos sencers",
"col_hut": "Registres HUT",
"col_hut_places": "Places HUT",
"col_euro_m2": "€/m² (2024)",
"col_source": "Font",
"source_airbnb": "Airbnb (2026-06)",
"source_hut": "HUT (2026-07)",
"source_ibestat": "IBESTAT (2019)",
"cta_map": "Veure al mapa",
"mallorca": {
"title": "Mallorca",
"intro": "L'illa grossa. La capital, Palma, concentra gairebé tota la pressió.",
"stat_listings_label": "Listings Airbnb",
"stat_entire_label": "Pisos sencers",
"stat_hosts_label": "Hosts únics",
"stat_eurom2_label": "€/m² compraventa",
"narrative": "14.644 pisos oferts a Airbnb, 13.925 dels quals són pisos sencers. Un de cada quatre pisos sencers que es lloguen a Mallorca estan a Airbnb. 8.458 hosts concentren l'oferta — la meitat són multi-propietaris. La pressió s'estén a municipis costaners com Calvià, Alcúdia, Pollença i Manacor."
},
"menorca": {
"title": "Menorca",
"intro": "Reserva de Biosfera, però el lloguer temporal s'ha menjat el pati.",
"stat_listings_label": "Listings Airbnb",
"stat_entire_label": "Pisos sencers",
"stat_hosts_label": "Hosts únics",
"stat_eurom2_label": "€/m² compraventa",
"narrative": "3.531 listings actius, 3.407 dels quals són pisos sencers (96%). La pressió és forta a Ciutadella i Maó, però el pati — l'habitatge permanent — s'ha omplert de lloguer temporal. La Reserva de Biosfera no ha blindat el mercat residencial."
},
"eivissa": {
"title": "Eivissa",
"intro": "La que té més places legals per habitant. I la bretxa més gran entre legal i real.",
"stat_hut_label": "Registres HUT",
"stat_hut_places_label": "Places HUT",
"stat_sant_josep_label": "A Sant Josep",
"stat_eurom2_label": "€/m² compraventa",
"narrative": "El Govern reconeix 2.364 registres HUT amb 18.184 places a l'illa. Però el 40% es concentren a Sant Josep de sa Talaia — un municipi rural de 28.000 habitants. A Eivissa capital (50.000 habitants) només 41 registres amb 236 places. On són les 7.000 places que s'ofereixen a Airbnb a Eivissa si el registre legal només en compta 236 al casc urbà?"
},
"formentera": {
"title": "Formentera",
"intro": "12.000 residents. Més places turístiques que habitants.",
"stat_estab_label": "Establiments",
"stat_places_label": "Places",
"stat_ratio_label": "Places / residents",
"stat_eurom2_label": "€/m² compraventa",
"narrative": "1.375 establiments, 14.935 places. Una illa sencera amb més places turístiques que residents. IBESTAT només publica dades agregades a nivell d'illa — la pressió concreta dins de cada nucli (Sant Francesc, Es Pujols, La Savina) no es pot desglossar amb les dades públiques disponibles."
}
}
}
+55 -1
View File
@@ -8,7 +8,8 @@
"nav": {
"home": "Inicio",
"map": "Mapa",
"methodology": "Metodología"
"methodology": "Metodología",
"islands": "Por isla"
},
"lang_toggle": {
"label": "Idioma",
@@ -122,5 +123,58 @@
"of_total": "del total",
"people": "persones",
"back_to_home": "Volver al inicio"
},
"islands": {
"title": "Por isla",
"lead": "Cada isla tiene una realidad distinta. Aquí las ponemos en paralelo con los mismos datos, las mismas fuentes y las mismas limitaciones.",
"section_compare_title": "Comparativa rápida",
"col_island": "Isla",
"col_sections": "Secciones",
"col_listings": "Listings Airbnb",
"col_entire": "Pisos enteros",
"col_hut": "Registros HUT",
"col_hut_places": "Plazas HUT",
"col_euro_m2": "€/m² (2024)",
"col_source": "Fuente",
"source_airbnb": "Airbnb (2026-06)",
"source_hut": "HUT (2026-07)",
"source_ibestat": "IBESTAT (2019)",
"cta_map": "Ver en el mapa",
"mallorca": {
"title": "Mallorca",
"intro": "La isla grande. La capital, Palma, concentra casi toda la presión.",
"stat_listings_label": "Listings Airbnb",
"stat_entire_label": "Pisos enteros",
"stat_hosts_label": "Hosts únicos",
"stat_eurom2_label": "€/m² compraventa",
"narrative": "14.644 pisos ofertados en Airbnb, 13.925 de los cuales son pisos enteros. Uno de cada cuatro pisos enteros que se alquilan en Mallorca están en Airbnb. 8.458 hosts concentran la oferta — la mitad son multi-propietarios. La presión se extiende a municipios costeros como Calvià, Alcúdia, Pollença y Manacor."
},
"menorca": {
"title": "Menorca",
"intro": "Reserva de la Biosfera, pero el alquiler temporal se ha comido el patio.",
"stat_listings_label": "Listings Airbnb",
"stat_entire_label": "Pisos enteros",
"stat_hosts_label": "Hosts únicos",
"stat_eurom2_label": "€/m² compraventa",
"narrative": "3.531 listings activos, 3.407 de los cuales son pisos enteros (96%). La presión es fuerte en Ciutadella y Maó, pero el patio — la vivienda permanente — se ha llenado de alquiler temporal. La Reserva de la Biosfera no ha blindado el mercado residencial."
},
"eivissa": {
"title": "Ibiza",
"intro": "La que tiene más plazas legales por habitante. Y la brecha más grande entre lo legal y lo real.",
"stat_hut_label": "Registros HUT",
"stat_hut_places_label": "Plazas HUT",
"stat_sant_josep_label": "En Sant Josep",
"stat_eurom2_label": "€/m² compraventa",
"narrative": "El Govern reconoce 2.364 registros HUT con 18.184 plazas en la isla. Pero el 40% se concentran en Sant Josep de sa Talaia — un municipio rural de 28.000 habitantes. En Ibiza capital (50.000 habitantes) solo 41 registros con 236 plazas. ¿Dónde están las 7.000 plazas que se ofertan en Airbnb en Ibiza si el registro legal solo cuenta 236 en el casco urbano?"
},
"formentera": {
"title": "Formentera",
"intro": "12.000 residentes. Más plazas turísticas que habitantes.",
"stat_estab_label": "Establecimientos",
"stat_places_label": "Plazas",
"stat_ratio_label": "Plazas / residentes",
"stat_eurom2_label": "€/m² compraventa",
"narrative": "1.375 establecimientos, 14.935 plazas. Una isla entera con más plazas turísticas que residentes. IBESTAT solo publica datos agregados a nivel de isla — la presión concreta dentro de cada núcleo (Sant Francesc, Es Pujols, La Savina) no se puede desglosar con los datos públicos disponibles."
}
}
}
+214
View File
@@ -0,0 +1,214 @@
---
export const prerender = false;
import Base from "../layouts/Base.astro";
import { getDataset } from "../data/dataset";
import { getLangFromAstroCookies, t, type Lang } from "../i18n";
const lang: Lang = getLangFromAstroCookies(Astro.cookies);
const dataset = getDataset();
interface CardStat {
value: string;
labelKey: string;
sourceKey?: "source_airbnb" | "source_hut" | "source_ibestat";
}
interface IslandCard {
key: "mallorca" | "menorca" | "eivissa" | "formentera";
sectionCount: number;
dataSourceKey: "source_airbnb" | "source_hut" | "source_ibestat";
stats: CardStat[];
}
const mallorcaRow = dataset.resumen_por_isla.find((r) => r.isla === "Mallorca")!;
const menorcaRow = dataset.resumen_por_isla.find((r) => r.isla === "Menorca")!;
const eivissaRow = dataset.resumen_por_isla.find((r) => r.isla === "Eivissa")!;
const formenteraRow = dataset.resumen_por_isla.find((r) => r.isla === "Formentera")!;
const EUROM2 = {
mallorca: 3500,
menorca: 2748,
eivissa: 4331,
formentera: 7353,
} as const;
const fmt = new Intl.NumberFormat(lang === "ca" ? "ca-ES" : "es-ES");
const fmtEur = (n: number) => `${fmt.format(n)} €`;
const cards: IslandCard[] = [
{
key: "mallorca",
sectionCount: mallorcaRow.secciones,
dataSourceKey: "source_airbnb",
stats: [
{ value: fmt.format(mallorcaRow.airbnb_listings), labelKey: "islands.mallorca.stat_listings_label" },
{ value: fmt.format(mallorcaRow.airbnb_entire_homes), labelKey: "islands.mallorca.stat_entire_label" },
{ value: fmt.format(mallorcaRow.airbnb_hosts_unicos), labelKey: "islands.mallorca.stat_hosts_label" },
{ value: fmtEur(EUROM2.mallorca), labelKey: "islands.mallorca.stat_eurom2_label" },
],
},
{
key: "menorca",
sectionCount: menorcaRow.secciones,
dataSourceKey: "source_airbnb",
stats: [
{ value: fmt.format(menorcaRow.airbnb_listings), labelKey: "islands.menorca.stat_listings_label" },
{ value: fmt.format(menorcaRow.airbnb_entire_homes), labelKey: "islands.menorca.stat_entire_label" },
{ value: fmt.format(menorcaRow.airbnb_hosts_unicos), labelKey: "islands.menorca.stat_hosts_label" },
{ value: fmtEur(EUROM2.menorca), labelKey: "islands.menorca.stat_eurom2_label" },
],
},
{
key: "eivissa",
sectionCount: eivissaRow.secciones,
dataSourceKey: "source_hut",
stats: [
{ value: fmt.format(eivissaRow.hut_registros), labelKey: "islands.eivissa.stat_hut_label" },
{ value: fmt.format(eivissaRow.hut_plazas), labelKey: "islands.eivissa.stat_hut_places_label" },
{ value: `40% (7.296)`, labelKey: "islands.eivissa.stat_sant_josep_label" },
{ value: fmtEur(EUROM2.eivissa), labelKey: "islands.eivissa.stat_eurom2_label" },
],
},
{
key: "formentera",
sectionCount: formenteraRow.secciones,
dataSourceKey: "source_ibestat",
stats: [
{ value: fmt.format(formenteraRow.hut_registros), labelKey: "islands.formentera.stat_estab_label" },
{ value: fmt.format(formenteraRow.hut_plazas), labelKey: "islands.formentera.stat_places_label" },
{ value: "1,24", labelKey: "islands.formentera.stat_ratio_label" },
{ value: fmtEur(EUROM2.formentera), labelKey: "islands.formentera.stat_eurom2_label" },
],
},
];
const description = t(lang, "islands.lead");
---
<Base
lang={lang}
active="islands"
title={t(lang, "islands.title")}
description={description}
>
<section class="bg-gradient-to-b from-orange-50 to-stone-50 border-b border-stone-200">
<div class="max-w-4xl mx-auto px-4 py-12 sm:py-16 text-center">
<h1 class="text-3xl sm:text-4xl font-bold text-stone-900 mb-4">
{t(lang, "islands.title")}
</h1>
<p class="text-base sm:text-lg text-stone-700 max-w-2xl mx-auto">
{description}
</p>
</div>
</section>
<section class="max-w-6xl mx-auto px-4 py-12">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
{cards.map((card) => (
<article class="border border-stone-200 rounded-lg p-6 bg-white flex flex-col">
<header class="mb-4">
<h2 class="text-2xl font-bold text-orange-700">
{t(lang, `islands.${card.key}.title`)}
</h2>
<p class="text-sm text-stone-500 mt-1">
{t(lang, `islands.${card.key}.intro`)}
</p>
</header>
<dl class="grid grid-cols-2 gap-4 mb-4">
{card.stats.map((s) => (
<div>
<dt class="text-xs uppercase tracking-wide text-stone-500">
{t(lang, s.labelKey)}
</dt>
<dd class="text-2xl font-bold text-stone-900 tabular-nums">
{s.value}
</dd>
</div>
))}
</dl>
<p class="text-sm text-stone-700 leading-relaxed mb-4 flex-1">
{t(lang, `islands.${card.key}.narrative`)}
</p>
<footer class="flex items-center justify-between text-xs text-stone-500">
<span>
{fmt.format(card.sectionCount)} {t(lang, "home.col_sections").toLowerCase()} · {t(lang, `islands.${card.dataSourceKey}`)}
</span>
<a
href="/mapa"
class="text-sm font-medium text-orange-700 hover:text-orange-800"
>
{t(lang, "islands.cta_map")} →
</a>
</footer>
</article>
))}
</div>
</section>
<section class="max-w-6xl mx-auto px-4 py-12">
<h2 class="text-2xl sm:text-3xl font-bold text-stone-900 mb-6">
{t(lang, "islands.section_compare_title")}
</h2>
<div class="overflow-x-auto border border-stone-200 rounded-lg bg-white">
<table class="w-full text-sm">
<thead class="bg-stone-100 text-stone-700">
<tr>
<th class="text-left px-4 py-2 font-medium">{t(lang, "islands.col_island")}</th>
<th class="text-right px-4 py-2 font-medium">{t(lang, "islands.col_sections")}</th>
<th class="text-right px-4 py-2 font-medium">{t(lang, "islands.col_listings")}</th>
<th class="text-right px-4 py-2 font-medium">{t(lang, "islands.col_entire")}</th>
<th class="text-right px-4 py-2 font-medium">{t(lang, "islands.col_hut")}</th>
<th class="text-right px-4 py-2 font-medium">{t(lang, "islands.col_hut_places")}</th>
<th class="text-right px-4 py-2 font-medium">{t(lang, "islands.col_euro_m2")}</th>
<th class="text-left px-4 py-2 font-medium">{t(lang, "islands.col_source")}</th>
</tr>
</thead>
<tbody>
<tr class="border-t border-stone-200">
<td class="px-4 py-2 font-medium">{t(lang, "islands.mallorca.title")}</td>
<td class="px-4 py-2 text-right tabular-nums">{fmt.format(mallorcaRow.secciones)}</td>
<td class="px-4 py-2 text-right tabular-nums">{fmt.format(mallorcaRow.airbnb_listings)}</td>
<td class="px-4 py-2 text-right tabular-nums">{fmt.format(mallorcaRow.airbnb_entire_homes)}</td>
<td class="px-4 py-2 text-right tabular-nums text-stone-400">—</td>
<td class="px-4 py-2 text-right tabular-nums text-stone-400">—</td>
<td class="px-4 py-2 text-right tabular-nums">{fmtEur(EUROM2.mallorca)}</td>
<td class="px-4 py-2 text-stone-500 text-xs">{t(lang, "islands.source_airbnb")}</td>
</tr>
<tr class="border-t border-stone-200">
<td class="px-4 py-2 font-medium">{t(lang, "islands.menorca.title")}</td>
<td class="px-4 py-2 text-right tabular-nums">{fmt.format(menorcaRow.secciones)}</td>
<td class="px-4 py-2 text-right tabular-nums">{fmt.format(menorcaRow.airbnb_listings)}</td>
<td class="px-4 py-2 text-right tabular-nums">{fmt.format(menorcaRow.airbnb_entire_homes)}</td>
<td class="px-4 py-2 text-right tabular-nums text-stone-400">—</td>
<td class="px-4 py-2 text-right tabular-nums text-stone-400">—</td>
<td class="px-4 py-2 text-right tabular-nums">{fmtEur(EUROM2.menorca)}</td>
<td class="px-4 py-2 text-stone-500 text-xs">{t(lang, "islands.source_airbnb")}</td>
</tr>
<tr class="border-t border-stone-200">
<td class="px-4 py-2 font-medium">{t(lang, "islands.eivissa.title")}</td>
<td class="px-4 py-2 text-right tabular-nums">{fmt.format(eivissaRow.secciones)}</td>
<td class="px-4 py-2 text-right tabular-nums text-stone-400">—</td>
<td class="px-4 py-2 text-right tabular-nums text-stone-400">—</td>
<td class="px-4 py-2 text-right tabular-nums">{fmt.format(eivissaRow.hut_registros)}</td>
<td class="px-4 py-2 text-right tabular-nums">{fmt.format(eivissaRow.hut_plazas)}</td>
<td class="px-4 py-2 text-right tabular-nums">{fmtEur(EUROM2.eivissa)}</td>
<td class="px-4 py-2 text-stone-500 text-xs">{t(lang, "islands.source_hut")}</td>
</tr>
<tr class="border-t border-stone-200">
<td class="px-4 py-2 font-medium">{t(lang, "islands.formentera.title")}</td>
<td class="px-4 py-2 text-right tabular-nums">{fmt.format(formenteraRow.secciones)}</td>
<td class="px-4 py-2 text-right tabular-nums text-stone-400">—</td>
<td class="px-4 py-2 text-right tabular-nums text-stone-400">—</td>
<td class="px-4 py-2 text-right tabular-nums">{fmt.format(formenteraRow.hut_registros)}</td>
<td class="px-4 py-2 text-right tabular-nums">{fmt.format(formenteraRow.hut_plazas)}</td>
<td class="px-4 py-2 text-right tabular-nums">{fmtEur(EUROM2.formentera)}</td>
<td class="px-4 py-2 text-stone-500 text-xs">{t(lang, "islands.source_ibestat")}</td>
</tr>
</tbody>
</table>
</div>
<p class="text-xs text-stone-500 mt-3">
€/m²: dades de compraventa 2024 (Conferència OHIB, 12-13 nov 2025).
</p>
</section>
</Base>