diff --git a/scripts/routeMeta.mjs b/scripts/routeMeta.mjs index e6ecea0..0e135b9 100644 --- a/scripts/routeMeta.mjs +++ b/scripts/routeMeta.mjs @@ -13,6 +13,7 @@ import { PRICE_PER_TB_MONTH, PRICE_AMOUNT, PRICE_PER_TB_SHORT_EUR, + PRICE_PER_TB_SHORT_EUR_ES, } from "../src/lib/pricing.constants.mjs"; export const BASE_URL = "https://www.fil.one"; @@ -369,9 +370,9 @@ export const ROUTE_META = { }, "/lp/es/barcelona": { lang: "es", - title: `Fil One para Barcelona: Almacenamiento Europeo, ${PRICE_PER_TB_SHORT_EUR}, Sin Egress`, + title: `Fil One para Barcelona: Almacenamiento Europeo, ${PRICE_PER_TB_SHORT_EUR_ES}, Sin Egress`, description: - `Almacenamiento de objetos compatible con S3 para equipos en Barcelona. Soberanía de datos en la UE, cero comisiones de egress, a ${PRICE_PER_TB_SHORT_EUR}. Intégralo en tu stack actual en minutos.`, + `Almacenamiento de objetos compatible con S3 para equipos en Barcelona. Soberanía de datos en la UE, cero comisiones de egress, a ${PRICE_PER_TB_SHORT_EUR_ES}. Intégralo en tu stack actual en minutos.`, }, "/contact-sales": { title: "Contact Sales · Fil One", diff --git a/src/components/CtaBanner.tsx b/src/components/CtaBanner.tsx index 07d8eb0..c72f75e 100644 --- a/src/components/CtaBanner.tsx +++ b/src/components/CtaBanner.tsx @@ -13,6 +13,8 @@ interface CtaBannerProps { */ secondaryCta?: { label: string; href: string; onClick?: () => void }; note?: ReactNode; + /** Cap on the heading width (px). Longer translations need more room. */ + headingMaxWidth?: number; } /** @@ -20,7 +22,7 @@ interface CtaBannerProps { * and a breathing glow, a headline, and a glowing primary button. Tuned for * the closing section of a page (sits on white, above the footer). */ -const CtaBanner = ({ heading, subhead, cta, secondaryCta, note }: CtaBannerProps) => { +const CtaBanner = ({ heading, subhead, cta, secondaryCta, note, headingMaxWidth = 480 }: CtaBannerProps) => { const { ref, inView } = useInView({ threshold: 0.05 }); return ( @@ -53,7 +55,7 @@ const CtaBanner = ({ heading, subhead, cta, secondaryCta, note }: CtaBannerProps

{heading}

diff --git a/src/components/IntegrationsSection.tsx b/src/components/IntegrationsSection.tsx index 1bb4e9b..2dccd4f 100644 --- a/src/components/IntegrationsSection.tsx +++ b/src/components/IntegrationsSection.tsx @@ -1,3 +1,4 @@ +import type { ReactNode } from "react"; import { SectionLabel, SectionHeading, SectionSub } from "@/components/LandingPrimitives"; import { useInView } from "@/hooks/useInView"; @@ -10,13 +11,34 @@ const INTEGRATIONS = [ interface IntegrationsSectionProps { /** white (default) or the standard grey section treatment (zinc-50 + zinc-100 borders) */ tone?: "white" | "grey"; + /** Section eyebrow; defaults to English. */ + label?: ReactNode; + /** Section heading; defaults to English. */ + heading?: ReactNode; + /** Supporting line under the heading; defaults to English. */ + description?: ReactNode; + /** Docs link label; defaults to English. */ + ctaLabel?: ReactNode; } /** * Auto-scrolling marquee of supported integrations — S3-compatibility * reassurance shared by the /lp/price and Barcelona landing pages. + * + * Copy is overridable so the Spanish pages can share the section rather than + * inlining a translated copy of it; the defaults are the English strings. */ -const IntegrationsSection = ({ tone = "white" }: IntegrationsSectionProps) => { +const IntegrationsSection = ({ + tone = "white", + label = "Integrations", + heading = ( + <> + Works with your existing stack + + ), + description = "S3 API compatible. If it talks to AWS, it talks to us.", + ctaLabel = "View documentation →", +}: IntegrationsSectionProps) => { const { ref, inView } = useInView({ threshold: 0.05 }); return ( @@ -31,9 +53,9 @@ const IntegrationsSection = ({ tone = "white" }: IntegrationsSectionProps) => { className={`flex flex-col gap-10 items-center text-center w-full max-w-container mx-auto reveal${inView ? " in-view" : ""}`} >
- Integrations - Works with your existing stack - S3 API compatible. If it talks to AWS, it talks to us. + {label} + {heading} + {description}
@@ -54,7 +76,7 @@ const IntegrationsSection = ({ tone = "white" }: IntegrationsSectionProps) => {
- View documentation → + {ctaLabel}
diff --git a/src/components/PriceComparisonTable.tsx b/src/components/PriceComparisonTable.tsx index 04536da..ec01ff0 100644 --- a/src/components/PriceComparisonTable.tsx +++ b/src/components/PriceComparisonTable.tsx @@ -39,9 +39,25 @@ interface PriceComparisonTableProps { centerFootnote?: boolean; } +/** + * Parse a formatted currency string to a number, tolerating both the English + * ("€1,234.56") and Spanish ("1.234,56 €") conventions. Whichever of "." or "," + * comes last is the decimal separator when 1 to 2 digits follow it; otherwise + * every separator is thousands grouping. Returns NaN for non-numeric values, + * which falls through to the neutral tone. + */ +const parseAmount = (value: string) => { + const digits = value.replace(/[^0-9.,-]/g, ""); + const lastSep = Math.max(digits.lastIndexOf("."), digits.lastIndexOf(",")); + if (lastSep === -1) return parseFloat(digits); + const trailing = digits.length - lastSep - 1; + if (trailing < 1 || trailing > 2) return parseFloat(digits.replace(/[.,]/g, "")); + return parseFloat(`${digits.slice(0, lastSep).replace(/[.,]/g, "")}.${digits.slice(lastSep + 1)}`); +}; + /** Zero reads as success, a large charge as danger, anything else neutral. */ const valueTone = (value: string) => { - const n = parseFloat(value.replace(/[^0-9.-]/g, "")); + const n = parseAmount(value); if (n === 0) return "text-success-700"; if (n > 50) return "text-danger-600"; return "text-zinc-600"; diff --git a/src/lib/pricing.constants.d.mts b/src/lib/pricing.constants.d.mts index e09f357..298a308 100644 --- a/src/lib/pricing.constants.d.mts +++ b/src/lib/pricing.constants.d.mts @@ -11,3 +11,9 @@ export const PRICE_PER_TB_SHORT_EUR: string; export const PRICE_PER_TB_MONTH_EUR: string; export const EUR_USD_RATE: number; export const EUR_USD_RATE_SOURCE: string; +export function eurEs(amount: number, decimals?: number): string; +export const PRICE_DISPLAY_EUR_ES: string; +export const PRICE_PER_TB_SHORT_EUR_ES: string; +export const PRICE_PER_TB_MONTH_EUR_ES: string; +export const EUR_USD_RATE_ES: string; +export const EUR_USD_RATE_SOURCE_ES: string; diff --git a/src/lib/pricing.constants.mjs b/src/lib/pricing.constants.mjs index fa42bca..735a963 100644 --- a/src/lib/pricing.constants.mjs +++ b/src/lib/pricing.constants.mjs @@ -55,3 +55,29 @@ export const EUR_USD_RATE = 1.17; /** Where and when EUR_USD_RATE was taken, for the comparison-table footnote. */ export const EUR_USD_RATE_SOURCE = "ECB rate, May 2026"; + +/* ── Spanish-locale EUR formatting ────────────────────────────────────────── + * Spanish writes a decimal comma with the symbol after the number and a + * non-breaking space ("4,99 €"), not "€4.99". The Spanish pages format every + * figure this way, so the helper is shared rather than hand-written per page. + * ─────────────────────────────────────────────────────────────────────────── */ + +/** Format a EUR amount the Spanish way, e.g. 49.9 -> "49,90 €". Pass + * `decimals: 0` for whole-euro figures like "197 €". */ +export const eurEs = (amount, decimals = 2) => + `${amount.toFixed(decimals).replace(".", ",")} €`; + +/** The bare EUR price, Spanish format, e.g. "4,99 €". */ +export const PRICE_DISPLAY_EUR_ES = eurEs(PRICE_PER_TB_EUR); + +/** The short per-TB EUR rate, Spanish format, e.g. "4,99 €/TB". */ +export const PRICE_PER_TB_SHORT_EUR_ES = `${PRICE_DISPLAY_EUR_ES}/TB`; + +/** The full per-TB EUR rate for inline Spanish copy, e.g. "4,99 €/TB al mes". */ +export const PRICE_PER_TB_MONTH_EUR_ES = `${PRICE_PER_TB_SHORT_EUR_ES} al mes`; + +/** EUR_USD_RATE in Spanish format with the symbol after the number, "1,17 $". */ +export const EUR_USD_RATE_ES = `${EUR_USD_RATE.toFixed(2).replace(".", ",")} $`; + +/** Spanish rendering of EUR_USD_RATE_SOURCE, for the ES comparison footnote. */ +export const EUR_USD_RATE_SOURCE_ES = "BCE, mayo de 2026"; diff --git a/src/lib/pricing.ts b/src/lib/pricing.ts index 765cf44..69b9e25 100644 --- a/src/lib/pricing.ts +++ b/src/lib/pricing.ts @@ -33,6 +33,18 @@ export { EUR_USD_RATE, /** Where and when EUR_USD_RATE was taken. */ EUR_USD_RATE_SOURCE, + /** Format a EUR amount the Spanish way, e.g. 49.9 -> "49,90 €". */ + eurEs, + /** The bare EUR price, Spanish format, e.g. "4,99 €". */ + PRICE_DISPLAY_EUR_ES, + /** The short per-TB EUR rate, Spanish format, e.g. "4,99 €/TB". */ + PRICE_PER_TB_SHORT_EUR_ES, + /** The full per-TB EUR rate for Spanish copy, e.g. "4,99 €/TB al mes". */ + PRICE_PER_TB_MONTH_EUR_ES, + /** EUR_USD_RATE in Spanish format, e.g. "1,17 $". */ + EUR_USD_RATE_ES, + /** Spanish rendering of EUR_USD_RATE_SOURCE. */ + EUR_USD_RATE_SOURCE_ES, } from "./pricing.constants.mjs"; import { PRICE_PER_TB } from "./pricing.constants.mjs"; diff --git a/src/pages/BarcelonaLandingPageES.tsx b/src/pages/BarcelonaLandingPageES.tsx index bd112fe..c46675b 100644 --- a/src/pages/BarcelonaLandingPageES.tsx +++ b/src/pages/BarcelonaLandingPageES.tsx @@ -4,632 +4,268 @@ import Footer from "@/components/Footer"; import { useInView } from "@/hooks/useInView"; import { useSeo } from "@/hooks/useSeo"; import { useLang } from "@/hooks/useLang"; -import { GRID_SVG, SectionLabel, SectionHeading, SectionSub } from '@/components/LandingPrimitives'; +import { SectionLabel, SectionHeading, SectionSub } from "@/components/LandingPrimitives"; +import Hero from "@/components/Hero"; +import { Button } from "@/components/Button"; +import FeaturedInBar from "@/components/FeaturedInBar"; +import StatCard from "@/components/StatCard"; +import FeatureCard from "@/components/FeatureCard"; +import CtaBanner from "@/components/CtaBanner"; +import IntegrationsSection from "@/components/IntegrationsSection"; +import PriceComparisonTable, { + type PriceComparisonColumn, + type PriceComparisonRow, +} from "@/components/PriceComparisonTable"; +import { + PRICE_PER_TB_EUR, + PRICE_PER_TB_SHORT_EUR_ES, + EUR_USD_RATE_ES, + EUR_USD_RATE_SOURCE_ES, + eurEs, +} from "@/lib/pricing"; -// ─── Grid texture (matches Index.tsx hero) ───────────────────────────────────── +const SIGNUP_URL = "https://app.fil.one/login?screen_hint=signup"; +const SUPPORT_HREF = "/lp/es/soporte"; +const CONTACT_SALES_HREF = "/lp/es/contacto"; -// ─── Pricing table helpers ──────────────────────────────────────────────────── -// Colours egress/API cells: $0 → green, large fees → red, small fees → neutral -const valueColor = (val: string) => { - const n = parseFloat(val.replace(/[$€,]/g, "")); - if (n === 0) return "#16a34a"; // green - if (n > 50) return "#dc2626"; // red - return "#52525B"; // neutral -}; +/** The modelled workload: 10 TB stored, 10 TB egress, 500K operations a month. */ +const WORKLOAD_TB = 10; +const FIL_ONE_TOTAL = eurEs(PRICE_PER_TB_EUR * WORKLOAD_TB); + +const PRICING_COLUMNS: PriceComparisonColumn[] = [ + { key: "region", header: "Región" }, + { key: "storage", header: "Almacenamiento" }, + { key: "egress", header: "Egress", colorByValue: true }, + { key: "api", header: "API / ops", colorByValue: true }, + { key: "total", header: "Total / mes", total: true }, +]; -// ─── Pricing table data ──────────────────────────────────────────────────────── -const PRICING_ROWS = [ - { provider: "Fil One", region: "EU-West", storage: "€49.90", egress: "€0", api: "€0", total: "€49.90", isFilOne: true }, - { provider: "Backblaze B2", region: "eu-central-003 Amsterdam", storage: "€59.60", egress: "€0", api: "€0", total: "€59.60", isFilOne: false }, - { provider: "Wasabi", region: "eu-west-2 Paris", storage: "€59.90", egress: "€0", api: "€0", total: "€59.90", isFilOne: false }, - { provider: "AWS S3 Standard", region: "eu-south-2 Madrid", storage: "€197", egress: "€790", api: "€1.83", total: "€990", isFilOne: false }, +const PRICING_ROWS: PriceComparisonRow[] = [ + { + provider: "Fil One", + isFilOne: true, + values: { + region: "EU-West", + storage: FIL_ONE_TOTAL, + egress: eurEs(0, 0), + api: eurEs(0, 0), + total: FIL_ONE_TOTAL, + }, + }, + { + provider: "Backblaze B2", + values: { + region: "eu-central-003 Amsterdam", + storage: eurEs(59.6), + egress: eurEs(0, 0), + api: eurEs(0, 0), + total: eurEs(59.6), + }, + }, + { + provider: "Wasabi", + values: { + region: "eu-west-2 Paris", + storage: eurEs(59.9), + egress: eurEs(0, 0), + api: eurEs(0, 0), + total: eurEs(59.9), + }, + }, + { + provider: "AWS S3 Standard", + values: { + region: "eu-south-2 Madrid", + storage: eurEs(197, 0), + egress: eurEs(790, 0), + api: eurEs(1.83), + total: eurEs(990, 0), + }, + }, ]; -// ─── Integrations ────────────────────────────────────────────────────────────── -const INTEGRATIONS = [ - "Iconik", "LucidLink", "Veeam", "Rclone", "Restic", - "MSP360", "Premiere", "DaVinci Resolve", "Hugging Face", - "PyTorch", "Arq", "Duplicati", +const STATS = [ + { stat: PRICE_PER_TB_SHORT_EUR_ES, label: "Precio fijo mensual" }, + { stat: eurEs(0, 0), label: "Costes por egress" }, + // El 20× es sobre la factura total, que depende del egress, así que + // solo se cumple con un uso intensivo de lectura. La nota indica el escenario. + { + stat: "20×", + label: "Más barato que AWS", + note: `Con ${WORKLOAD_TB} TB de almacenamiento y ${WORKLOAD_TB} TB de egress`, + }, +]; + +const FEATURES = [ + { icon: Plug, title: "Compatibilidad inmediata con S3", desc: "La misma API, los mismos SDK y herramientas. Conecta tu flujo de trabajo a nuestro endpoint y sigue trabajando." }, + { icon: ArrowsOut, title: "Sin cargos por egress", desc: "Cada lectura es gratis, así que tu factura se mantiene plana sin importar cuánto uses el servicio." }, + // Espacio fino antes del %, según la convención del SI y la RAE. + { icon: ShieldCheck, title: "Once nueves de durabilidad", desc: "99,999999999 % de durabilidad, replicada en varias ubicaciones y monitorizada permanentemente." }, + { icon: Lock, title: "Object Lock y versionado", desc: "Modos de cumplimiento, periodos de retención y registros de auditoría a prueba de manipulaciones." }, + { icon: MapPin, title: "Tus datos nunca salen de la UE", desc: "La infraestructura de almacenamiento permanece dentro de las fronteras europeas." }, + { icon: Rocket, title: "Listo en cuestión de minutos", desc: "Genera tus claves de acceso, apunta tus herramientas a nuestro endpoint y empieza a subir datos." }, ]; -// ─── Page ────────────────────────────────────────────────────────────────────── const BarcelonaLandingPageES = () => { useLang("es"); useSeo({ - title: "Fil One para Barcelona: Almacenamiento Europeo, €4.99/TB, Sin Egress", + title: `Fil One para Barcelona: Almacenamiento Europeo, ${PRICE_PER_TB_SHORT_EUR_ES}, Sin Egress`, description: - "Almacenamiento de objetos compatible con S3 para equipos en Barcelona. Soberanía de datos en la UE, cero comisiones de egress, a €4.99/TB. Intégralo en tu stack actual en minutos.", + `Almacenamiento de objetos compatible con S3 para equipos en Barcelona. Soberanía de datos en la UE, cero comisiones de egress, a ${PRICE_PER_TB_SHORT_EUR_ES}. Intégralo en tu stack actual en minutos.`, canonical: "https://www.fil.one/lp/es/barcelona", }); - const { ref: posRef, inView: posInView } = useInView({ threshold: 0.05 }); - const { ref: pricingRef, inView: pricingInView } = useInView({ threshold: 0.05 }); - const { ref: featuresRef, inView: featuresInView } = useInView({ threshold: 0.05 }); - const { ref: integrationsRef, inView: integrationsInView } = useInView({ threshold: 0.05 }); - const { ref: ctaRef, inView: ctaInView } = useInView({ threshold: 0.05 }); + const { ref: posRef, inView: posInView } = useInView({ threshold: 0.05 }); + const { ref: pricingRef, inView: pricingInView } = useInView({ threshold: 0.05 }); + const { ref: featuresRef, inView: featuresInView } = useInView({ threshold: 0.05 }); return ( -
- +
+
{/* ── Hero ─────────────────────────────────────────────────────────── */} -
- {/* Blue radial glow */} -
+ + Almacenamiento europeo. +
+ {PRICE_PER_TB_SHORT_EUR_ES}, sin cargos por egress. + + } + description="Almacenamiento de objetos compatible con S3 que mantiene tus datos en Europa. Funciona con las herramientas que ya utilizas, sin necesidad de migraciones." + ctas={[{ label: "Empieza con 30 días gratis", href: SIGNUP_URL, variant: "primary", size: "lg", glow: true }]} + /> {/* ── Publications / Social proof ──────────────────────────────────── */} -
-
-

- Nuestra tecnología ha sido destacada en -

-
-
- {[0, 1].map((copy) => ( - - {["Fast Company", "CNBC", "Bloomberg", "Yahoo Finance", "VentureBeat"].map((pub) => ( - - {pub} - · - - ))} - - ))} -
-
-
-
+ - {/* ── Positioning ───────────────────────────────────────────────── */} -
+ {/* ── Positioning ───────────────────────────────────────────────────── */} +
Por qué Fil One - Un coste imbatible + + Un coste imbatible + - Compatible con S3, soberanía de datos en la UE y sin cargos por tráfico de salida. Sin costes ocultos ni sorpresas. + Compatible con S3, soberanía de datos en la UE y sin cargos por egress. Sin costes ocultos ni + sorpresas.
- {[ - { stat: "€4.99/TB", label: "Precio fijo mensual" }, - { stat: "€0", label: "Costes de salida" }, - { stat: "20×", label: "Más barato que AWS" }, - ].map(({ stat, label }) => ( -
-

{stat}

-

{label}

-
+ {STATS.map(({ stat, label, note }) => ( + ))}
{/* ── Pricing table ─────────────────────────────────────────────────── */} -
+
Precios - Tu factura mensual, de cuatro formas + + Tu factura mensual, de cuatro formas + - Un equipo de 10 TB en Barcelona, con 10 TB de tráfico de salida al mes y con 500.000 operaciones de objetos. + Un equipo en Barcelona con {WORKLOAD_TB} TB de almacenamiento, {WORKLOAD_TB} TB de egress al mes y + 500.000 operaciones sobre objetos.
- {/* Mobile: stacked cards (table below is md+ only) */} -
- {PRICING_ROWS.map((row) => ( -
-
- - {row.provider} - - {row.isFilOne && ( - - Tú - - )} -
-
- Región - {row.region} - Almacenamiento - {row.storage} - Egress - {row.egress} - API / ops - {row.api} - Total / mes - - {row.total} - -
-
- ))} -
- - {/* Desktop / tablet: full table */} -
- - - - {["Proveedor", "Región", "Almacenamiento", "Egress", "API / ops", "Total / mes"].map((h) => ( - - ))} - - - - {PRICING_ROWS.map((row) => ( - - {/* Provider */} - - {/* Region */} - - {/* Storage */} - - {/* Egress */} - - {/* API */} - - {/* Total */} - - - ))} - -
- {h} -
- {row.provider} - {row.isFilOne && ( - - Tú - - )} - - {row.region} - - {row.storage} - - {row.egress} - - {row.api} - - {row.total} -
-
-

- Los precios de la competencia se han convertido de USD utilizando el tipo de cambio de €1 = $1.17 (Tipo de cambio ECB, Mayo 2026). Fil One tiene un precio nativo en euros de 4.99 €/TB. -

+ + Los precios de los demás proveedores se han convertido de USD a EUR utilizando un tipo de cambio de + 1 € = {EUR_USD_RATE_ES} ({EUR_USD_RATE_SOURCE_ES}). Fil One tiene un precio nativo en euros de{" "} + {PRICE_PER_TB_SHORT_EUR_ES}. + + } + /> {/* Mid-page CTA after pricing table */}
- - Prueba gratis por 30 días - +
{/* ── Features ─────────────────────────────────────────────────────── */} -
-
+
+
- - Funcionalidades - -

- El S3 que esperabas -

-

- Compatible con todo lo que tu equipo ya utiliza. -

+ Funcionalidades + + El S3 que esperabas + + Compatible con todo lo que tu equipo ya utiliza.
- {[ - { icon: Plug, title: "Compatibilidad inmediata con S3", desc: "La misma API, los mismos SDKs y herramientas. Conecta tu flujo a nuestro endpoint y sigue trabajando." }, - { icon: ArrowsOut, title: "Sin cargos por tráfico de salida", desc: "Cada lectura es gratis, así que tu factura se mantiene plana sin importar cuánto uses el servicio." }, - { icon: ShieldCheck, title: "Once nueves de durabilidad", desc: "99,999999999% de durabilidad, replicada en varias ubicaciones y monitorizada permanentemente." }, - { icon: Lock, title: "Object Lock y versionado", desc: "Modos de cumplimiento, periodos de retención y registros de auditoría a prueba de manipulación." }, - { icon: MapPin, title: "Tus datos nunca salen de la UE", desc: "La infraestructura de almacenamiento permanece dentro de las fronteras europeas." }, - { icon: Rocket, title: "Listo en cuestión de minutos", desc: "Genera tus claves de acceso, configura tus herramientas existentes para que apunten a nuestro endpoint y empieza a subir datos." }, - ].map(({ icon: Icon, title, desc }) => ( -
( + -
- -
-
-

- {title} -

-

- {desc} -

-
-
+ icon={icon} + title={title} + description={desc} + className={`reveal${featuresInView ? " in-view" : ""}`} + /> ))}
{/* ── Integrations ──────────────────────────────────────────────────── */} -
-
-
- Integraciones - Funciona con tu stack actual - - Compatible con la API de S3. Si funciona con AWS, funciona con nosotros. - -
- -
-
- {[0, 1].map((copy) => ( -
- {INTEGRATIONS.map((name) => ( -
- {name} -
- ))} -
- ))} -
-
- - - Ver la documentación → - -
-
+ + Funciona con tu stack actual + + } + description="Compatible con la API de S3. Si funciona con AWS, funciona con nosotros." + ctaLabel="Ver documentación →" + /> {/* ── CTA Banner ────────────────────────────────────────────────────── */} -
-
-
- {/* White grid texture, drifting slowly */} - -
+
-
); };