diff --git a/app/contacto/page.tsx b/app/contacto/page.tsx
new file mode 100644
index 0000000..5c768f6
--- /dev/null
+++ b/app/contacto/page.tsx
@@ -0,0 +1,99 @@
+"use client";
+import { motion } from "framer-motion";
+import { Badge } from "@/components/ui/Badge";
+import { ContactoForm } from "@/components/forms/ContactoForm";
+import { FiMapPin, FiMail, FiPhone } from "react-icons/fi";
+import { useLanguage } from "@/context/LanguageContext";
+
+export default function ContactoPage() {
+ const { t } = useLanguage();
+
+ const infoContacto = [
+ {
+ icon: ,
+ label: t.contacto.info.address,
+ value: "Av. de Madrid, 138, Sants-Montjuïc, 08028 Barcelona",
+ href: "https://www.google.com/maps/search/?api=1&query=Av.+de+Madrid,+138,+08028+Barcelona",
+ },
+ {
+ icon: ,
+ label: t.contacto.info.email,
+ value: "jordi@itemopina.com",
+ href: "mailto:jordi@itemopina.com",
+ isEmail: true,
+ },
+ {
+ icon: ,
+ label: t.contacto.info.phone,
+ value: "650 066 923",
+ href: "tel:+34650066923",
+ },
+ ];
+
+ // Lógica para resaltar la última palabra del título en color ocean
+ const titleWords = t.contacto.heroTitle.split(" ");
+ const lastWord = titleWords.pop();
+ const mainTitle = titleWords.join(" ");
+
+ return (
+
+
+
+
+ {/* Columna de Información */}
+
+ {t.contacto.badge || "Contacto Directo"}
+
+
+ {mainTitle}
+ {lastWord}
+
+
+
+ {t.contacto.heroDesc}
+
+
+
+ {infoContacto.map((item, idx) => (
+
+ ))}
+
+
+
+ {/* Columna del Formulario */}
+
+
+ {t.contacto.form.title}
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/globals.css b/app/globals.css
index a2dc41e..cc7f677 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,26 +1,28 @@
@import "tailwindcss";
-:root {
- --background: #ffffff;
- --foreground: #171717;
+@theme {
+ --color-navy: #03045E;
+ --color-ocean: #0077B6;
+ --color-sky: #00B4D8;
+ --color-powder: #90E0EF;
+ --color-ice: #CAF0F8;
+ --font-sans: 'Manrope', sans-serif;
}
-@theme inline {
- --color-background: var(--background);
- --color-foreground: var(--foreground);
- --font-sans: var(--font-geist-sans);
- --font-mono: var(--font-geist-mono);
-}
-
-@media (prefers-color-scheme: dark) {
- :root {
- --background: #0a0a0a;
- --foreground: #ededed;
+@layer base {
+ /* Esto elimina la línea azul en todos los navegadores */
+ *, *:focus, *:active, *:focus-visible {
+ outline: none !important;
+ box-shadow: none !important;
+ ring: 0 !important;
+ }
+
+ input, textarea, select, button {
+ outline: none !important;
+ box-shadow: none !important;
}
}
-body {
- background: var(--background);
- color: var(--foreground);
- font-family: Arial, Helvetica, sans-serif;
-}
+@utility border-crisp {
+ border: 1px solid rgba(202, 240, 248, 0.6);
+}
\ No newline at end of file
diff --git a/app/insights/page.tsx b/app/insights/page.tsx
new file mode 100644
index 0000000..a2a1e28
--- /dev/null
+++ b/app/insights/page.tsx
@@ -0,0 +1,102 @@
+"use client";
+import { Badge } from "@/components/ui/Badge";
+import { Card } from "@/components/ui/Card";
+import { motion } from "framer-motion";
+import Image from "next/image";
+import { useLanguage } from "@/context/LanguageContext";
+
+export default function InsightsPage() {
+ const { t, locale } = useLanguage();
+
+ // Mapeo de meses para las fechas de los artículos
+ const months = {
+ es: { mayo: "Mayo", abril: "Abril", marzo: "Marzo" },
+ ca: { mayo: "Maig", abril: "Abril", marzo: "Març" },
+ en: { mayo: "May", abril: "April", marzo: "March" }
+ };
+
+ const articulos = [
+ {
+ titulo: locale === 'en' ? "The new post-pandemic consumer in Spain" :
+ locale === 'ca' ? "El nou consumidor post-pandèmia a Espanya" :
+ "El nuevo consumidor post-pandemia en España",
+ categoria: t.insights.categories.tendencias,
+ imagen: "https://images.unsplash.com/photo-1551434678-e076c223a692?q=80&w=2070&auto=format&fit=crop",
+ fecha: `${months[locale].mayo} 2026`,
+ size: "large"
+ },
+ {
+ titulo: locale === 'en' ? "Agile methodologies in fieldwork" :
+ locale === 'ca' ? "Metodologies àgils en el treball de camp" :
+ "Metodologías ágiles en el trabajo de campo",
+ categoria: t.insights.categories.innovacion,
+ imagen: "https://images.unsplash.com/photo-1460925895917-afdab827c52f?q=80&w=2015&auto=format&fit=crop",
+ fecha: `${months[locale].abril} 2026`,
+ size: "small"
+ },
+ {
+ titulo: locale === 'en' ? "Mystery Shopping: Beyond compliance" :
+ locale === 'ca' ? "Mistery Shopping: Més enllà del compliment" :
+ "Mistery Shopping: Más allá del cumplimiento",
+ categoria: t.insights.categories.estrategia,
+ imagen: "https://images.unsplash.com/photo-1534452203293-494d7ddbf7e0?q=80&w=2072&auto=format&fit=crop",
+ fecha: `${months[locale].marzo} 2026`,
+ size: "small"
+ }
+ ];
+
+ // Dividimos el título para aplicar el color al final
+ const titleParts = t.insights.title.split('&');
+
+ return (
+
+
+
{t.insights.badge}
+
+ {titleParts[0]} & {titleParts[1]}
+
+
+ {t.insights.description}
+
+
+
+
+ {articulos.map((item, idx) => (
+
+
+
+
+
+
+ {item.categoria}
+
+
+
+
+
{item.fecha}
+
+ {item.titulo}
+
+
+ {t.insights.readMore}
+
+
+
+
+
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
index 976eb90..a5e90a2 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,20 +1,15 @@
import type { Metadata } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
+import { Manrope } from "next/font/google";
import "./globals.css";
+import { Navbar } from "@/components/layout/Navbar";
+import { Footer } from "@/components/layout/Footer";
+import { LanguageProvider } from "@/context/LanguageContext";
-const geistSans = Geist({
- variable: "--font-geist-sans",
- subsets: ["latin"],
-});
-
-const geistMono = Geist_Mono({
- variable: "--font-geist-mono",
- subsets: ["latin"],
-});
+const manrope = Manrope({ subsets: ["latin"] });
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: "ITEM OPINA | Inteligencia de Mercado",
+ description: "Estudios de mercado y consultoría con rigor metodológico.",
};
export default function RootLayout({
@@ -23,11 +18,16 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
-
{children}
+
+
+
+
+
+ {children}
+
+
+
+
);
-}
+}
\ No newline at end of file
diff --git a/app/nosotros/page.tsx b/app/nosotros/page.tsx
new file mode 100644
index 0000000..181fb95
--- /dev/null
+++ b/app/nosotros/page.tsx
@@ -0,0 +1,141 @@
+"use client";
+import { motion } from "framer-motion";
+import { Badge } from "@/components/ui/Badge";
+import { Card } from "@/components/ui/Card";
+import { Button } from "@/components/ui/Button";
+import Link from "next/link";
+import { useLanguage } from "@/context/LanguageContext";
+
+export default function NosotrosPage() {
+ const { t, locale } = useLanguage();
+
+ // Dividimos el título para aplicar el color al final (Desde 2004 / Des de 2004 / Since 2004)
+ const titleParts = t.nosotros.title.split('.');
+
+ return (
+
+ {/* Hero Narrativo */}
+
+ {t.nosotros.badge}
+
+
+ {titleParts[0]}.
+ {titleParts[1]}
+
+
+
+ {t.nosotros.desc}
+
+
+
+ {/* Sección Historia (Layout Asimétrico) */}
+
+
+
+
+
+
+ {t.nosotros.history.title}
+
+
+ {locale === 'en' ? "Get to know us. From our foundation in Barcelona to our national consolidation." :
+ locale === 'ca' ? "Coneix-nos. Des de la nostra fundació a Barcelona fins a la nostra consolidació nacional." :
+ "Conócenos. Desde nuestra fundación en Barcelona hasta nuestra consolidación a nivel nacional."}
+
+
+
+
+
+
+ {t.nosotros.history.p1}
+
+
+ {t.nosotros.history.p2}
+
+
+ {t.nosotros.history.p3}
+
+
+
+
+
+ {/* Sección Misión con Cards horizontales */}
+
+
+
+ {locale === 'en' ? "Our Mission" : locale === 'ca' ? "La nostra Missió" : "Nuestra Misión"}
+
+
+
+
+ 01.
+
+
{t.nosotros.mission.m1Title}
+
+ {t.nosotros.mission.m1Desc}
+
+
+
+
+
+ 02.
+
+
{t.nosotros.mission.m2Title}
+
+ {t.nosotros.mission.m2Desc}
+
+
+
+
+
+ 03.
+
+
{t.nosotros.mission.m3Title}
+
+ {t.nosotros.mission.m3Desc}
+
+
+
+
+
+
+
+ {/* CTA Final */}
+
+
+
+
+ {locale === 'en' ? "A team ready to understand your market." :
+ locale === 'ca' ? "Un equip disposat a entendre el teu mercat." :
+ "Un equipo dispuesto a entender tu mercado."}
+
+
+ {locale === 'en' ? "Let's talk about your next research project. Tell us what you need to discover." :
+ locale === 'ca' ? "Parlem del teu proper projecte de recerca. Explica'ns què necessites descobrir." :
+ "Hablemos de tu próximo proyecto de investigación. Cuéntanos qué necesitas descubrir."}
+
+
+
+ {t.navbar.contacto}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/page.tsx b/app/page.tsx
index 3f36f7c..83f72ad 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,65 +1,247 @@
-import Image from "next/image";
+"use client";
+import { motion } from "framer-motion";
+import { BentoGrid } from "@/components/ui/BentoGrid";
+import { Card } from "@/components/ui/Card";
+import { Button } from "@/components/ui/Button";
+import { Badge } from "@/components/ui/Badge";
+import Link from "next/link";
+import { useLanguage } from "@/context/LanguageContext";
export default function Home() {
+ const { t, locale } = useLanguage();
+
+ // Helper para las etiquetas de los meses en el SVG según el idioma
+ const months = {
+ es: ["ENE", "MAR", "JUN", "SEP", "DIC"],
+ ca: ["GEN", "MAR", "JUN", "SET", "DES"],
+ en: ["JAN", "MAR", "JUN", "SEP", "DEC"]
+ };
+
return (
-
-
-
-
-
- To get started, edit the page.tsx file.
-
-
- Looking for a starting point or more instructions? Head over to{" "}
-
- Templates
- {" "}
- or the{" "}
-
- Learning
- {" "}
- center.
-
-
-
);
-}
+}
\ No newline at end of file
diff --git a/app/participa/page.tsx b/app/participa/page.tsx
new file mode 100644
index 0000000..0a29383
--- /dev/null
+++ b/app/participa/page.tsx
@@ -0,0 +1,91 @@
+"use client";
+import { motion } from "framer-motion";
+import { Badge } from "@/components/ui/Badge";
+import { ParticipaForm } from "@/components/forms/ParticipaForm";
+import { FiCheck } from "react-icons/fi";
+import { useLanguage } from "@/context/LanguageContext";
+
+export default function ParticipaPage() {
+ const { t } = useLanguage();
+
+ // Mapeo dinámico de los beneficios desde el diccionario
+ const beneficios = [
+ {
+ titulo: t.participa.benefits.b1Title,
+ desc: t.participa.benefits.b1Desc,
+ },
+ {
+ titulo: t.participa.benefits.b2Title,
+ desc: t.participa.benefits.b2Desc,
+ },
+ {
+ titulo: t.participa.benefits.b3Title,
+ desc: t.participa.benefits.b3Desc,
+ },
+ ];
+
+ return (
+
+
+ {/* Columna Informativa (Izquierda) */}
+
+
+ {t.participa.badge}
+
+
+ {/* Usamos el título del diccionario */}
+ {t.participa.heroTitle.split(' ').slice(0, -2).join(' ')}
+
+ {t.participa.heroTitle.split(' ').slice(-2).join(' ')}
+
+
+
+
+ {t.participa.heroDesc}
+
+
+
+ {beneficios.map((item, idx) => (
+
+
+
+
+
+
{item.titulo}
+
{item.desc}
+
+
+ ))}
+
+
+
+
+ {/* Columna Formulario (Derecha) */}
+
+
+
+ {t.participa.formTitle}
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/servicios/page.tsx b/app/servicios/page.tsx
new file mode 100644
index 0000000..8bf8688
--- /dev/null
+++ b/app/servicios/page.tsx
@@ -0,0 +1,133 @@
+"use client";
+import { motion } from "framer-motion";
+import { Badge } from "@/components/ui/Badge";
+import { Card } from "@/components/ui/Card";
+import { Button } from "@/components/ui/Button";
+import { BentoGrid } from "@/components/ui/BentoGrid";
+import Link from "next/link";
+import Image from "next/image";
+import { useLanguage } from "@/context/LanguageContext";
+
+export default function ServiciosPage() {
+ const { t } = useLanguage();
+
+ return (
+
+ {/* Hero de Servicios */}
+
+ {t.servicios.heroBadge || "Cartera de Servicios"}
+
+
+ {t.servicios.hero}
+
+
+ {/* Descripción en dos columnas */}
+
+
+
+ {t.servicios.cat1}
+
+
+ {t.servicios.cat1Desc}
+
+
+
+
+
+ {t.servicios.cat2}
+
+
+ {t.servicios.cat2Desc}
+
+
+
+
+
+ {/* Grid Bento de Servicios */}
+
+
+
+
+ {/* Imagen 1 */}
+
+
+
+
+
+
+ {/* Consultoría */}
+
+ {t.servicios.items.consultoria}
+
+ {t.servicios.consultoriaDesc}
+
+
+
+
+ {t.servicios.btnGo || "Ir a la página"}
+
+
+
+
+
+ {/* Red de Campo */}
+
+ {t.servicios.items.red}
+
+ {t.servicios.redDesc}
+
+
+
+
+ {t.servicios.btnGo || "Ir a la página"}
+
+
+
+
+
+ {/* Estudios (Bloque destacado) */}
+
+ {t.servicios.items.estudios}
+
+ {t.servicios.estudiosDesc}
+
+
+
+
+ {t.servicios.btnGo || "Ir a la página"}
+
+
+
+
+
+ {/* Imagen 2 */}
+
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/forms/ContactoForm.tsx b/components/forms/ContactoForm.tsx
new file mode 100644
index 0000000..cbfbf9e
--- /dev/null
+++ b/components/forms/ContactoForm.tsx
@@ -0,0 +1,86 @@
+"use client";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { ContactoSchema, ContactoFormData } from "@/utils/validations";
+import { useFormSubmit } from "@/hooks/useForm";
+import { Input } from "../ui/Input";
+import { Button } from "../ui/Button";
+import { Checkbox } from "../ui/Checkbox";
+import { Textarea } from "../ui/Textarea";
+import { motion, AnimatePresence } from "framer-motion";
+import { useLanguage } from "@/context/LanguageContext";
+
+export const ContactoForm = () => {
+ const { t, locale } = useLanguage();
+ const { submit, isSubmitting, success, error } = useFormSubmit("contactos");
+
+ const { register, handleSubmit, formState: { errors }, reset } = useForm({
+ resolver: zodResolver(ContactoSchema),
+ });
+
+ const onSubmit = async (data: ContactoFormData) => {
+ await submit(data);
+ if (!error) reset();
+ };
+
+ return (
+
+
+ {success ? (
+
+
+ {t.contacto.success.title}
+
+
+ {t.contacto.success.desc}
+
+
+ ) : (
+
+ )}
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/forms/ParticipaForm.tsx b/components/forms/ParticipaForm.tsx
new file mode 100644
index 0000000..96bce65
--- /dev/null
+++ b/components/forms/ParticipaForm.tsx
@@ -0,0 +1,305 @@
+"use client";
+import { useState, useEffect } from "react";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { ParticipaSchema, ParticipaFormData } from "@/utils/validations";
+import { Input } from "../ui/Input";
+import { Button } from "../ui/Button";
+import { Checkbox } from "../ui/Checkbox";
+import { Textarea } from "../ui/Textarea";
+import { motion, AnimatePresence } from "framer-motion";
+import { useLanguage } from "@/context/LanguageContext";
+import { supabase } from "@/lib/supabase/client";
+
+export const ParticipaForm = () => {
+ const { t, locale } = useLanguage();
+
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [success, setSuccess] = useState(false);
+ const [errorMsg, setErrorMsg] = useState(null);
+
+ const [campanas, setCampanas] = useState([]);
+
+ // Estados para manejar las preguntas dinámicas (El Screener)
+ const [preguntasDinamicas, setPreguntasDinamicas] = useState([]);
+ const [respuestasDinamicas, setRespuestasDinamicas] = useState>({});
+
+ const {
+ register,
+ handleSubmit,
+ watch,
+ formState: { errors },
+ reset
+ } = useForm({
+ resolver: zodResolver(ParticipaSchema),
+ defaultValues: {
+ interes: "" as any
+ }
+ });
+
+ // Vigila en tiempo real qué campaña ha seleccionado el usuario
+ const campanaSeleccionadaId = watch("interes");
+
+ useEffect(() => {
+ const fetchCampanas = async () => {
+ try {
+ const { data, error } = await supabase
+ .from("campanas")
+ .select(`
+ id,
+ nombre,
+ encuestas ( configuracion )
+ `)
+ .eq('estado', 'activa');
+
+ if (data && !error) {
+ setCampanas(data);
+ }
+ } catch (err) {
+ console.error("Error al cargar las campañas:", err);
+ }
+ };
+
+ fetchCampanas();
+ }, []);
+
+ // Efecto que se dispara cuando el usuario cambia de campaña en el select
+ useEffect(() => {
+ if (campanaSeleccionadaId) {
+ const campana = campanas.find(c => c.id === campanaSeleccionadaId);
+
+ if (campana && campana.encuestas && campana.encuestas.length > 0) {
+ const config = campana.encuestas[0].configuracion;
+ if (config && config.preguntas_filtro) {
+ setPreguntasDinamicas(config.preguntas_filtro);
+ setRespuestasDinamicas({});
+ return;
+ }
+ }
+ }
+ setPreguntasDinamicas([]);
+ setRespuestasDinamicas({});
+ }, [campanaSeleccionadaId, campanas]);
+
+ // Manejador para los cambios en los inputs dinámicos
+ const handleDynamicChange = (preguntaId: string, valor: any) => {
+ setRespuestasDinamicas(prev => ({
+ ...prev,
+ [preguntaId]: valor
+ }));
+ };
+
+ const onSubmit = async (data: ParticipaFormData) => {
+ setIsSubmitting(true);
+ setErrorMsg(null);
+
+ const payload: any = {
+ datos_participante: {
+ nombre: data.nombre,
+ apellidos: data.apellidos,
+ email: data.email,
+ telefono: data.telefono,
+ perfil_demografico: {
+ provincia: data.provincia,
+ edad: Number(data.edad),
+ mensaje: data.mensaje || ""
+ }
+ }
+ };
+
+ if (data.interes && data.interes !== "") {
+ payload.campana_id = data.interes;
+ payload.datos_especificos = respuestasDinamicas;
+ }
+
+ try {
+ const { data: responseData, error } = await supabase.functions.invoke('validate-participant-data', {
+ body: payload
+ });
+
+ if (error) {
+ throw new Error(error.message || "Error al procesar la solicitud en el servidor.");
+ }
+
+ setSuccess(true);
+ reset();
+ } catch (err: any) {
+ console.error("Error en Screener:", err);
+ setErrorMsg(err.message || "Hubo un problema de conexión al enviar tus datos.");
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/components/layout/Footer.tsx b/components/layout/Footer.tsx
new file mode 100644
index 0000000..e5622e7
--- /dev/null
+++ b/components/layout/Footer.tsx
@@ -0,0 +1,46 @@
+"use client";
+import Link from "next/link";
+import { useLanguage } from "@/context/LanguageContext";
+
+export const Footer = () => {
+ const { t } = useLanguage();
+ const currentYear = new Date().getFullYear();
+
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/components/layout/Navbar.tsx b/components/layout/Navbar.tsx
new file mode 100644
index 0000000..3cb9ff6
--- /dev/null
+++ b/components/layout/Navbar.tsx
@@ -0,0 +1,145 @@
+"use client";
+import { useState, useEffect } from "react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { Button } from "../ui/Button";
+import { motion, AnimatePresence } from "framer-motion";
+import { FiMenu, FiX, FiGlobe } from "react-icons/fi";
+import { useLanguage } from "@/context/LanguageContext";
+
+export const Navbar = () => {
+ const [isOpen, setIsOpen] = useState(false);
+ const pathname = usePathname();
+ const { locale, setLocale, t } = useLanguage();
+
+ const isActive = (path: string) => pathname === path;
+
+ useEffect(() => {
+ setIsOpen(false);
+ }, [pathname]);
+
+ useEffect(() => {
+ if (isOpen) {
+ document.body.style.overflow = "hidden";
+ } else {
+ document.body.style.overflow = "unset";
+ }
+ }, [isOpen]);
+
+ const navLinks = [
+ { name: t.navbar.consultora, href: "/nosotros" },
+ { name: t.navbar.servicios, href: "/servicios" },
+ { name: t.navbar.participa, href: "/participa" },
+ { name: t.navbar.insights, href: "/insights" },
+ ];
+
+ return (
+
+
+ {/* Logo */}
+
+ Item Opina.
+
+
+ {/* Menú Central (Escritorio) */}
+
+ {navLinks.map((link) => (
+
+ {link.name}
+
+ ))}
+
+
+ {/* Idiomas y Acción (Escritorio) */}
+
+ {/* Selector de Idiomas */}
+
+ {(["es", "ca", "en"] as const).map((lang) => (
+ setLocale(lang)}
+ className={`text-[11px] font-black uppercase transition-all ${
+ locale === lang ? "text-ocean scale-110" : "text-navy/30 hover:text-navy"
+ }`}
+ >
+ {lang}
+
+ ))}
+
+
+
+
+ {t.navbar.contacto}
+
+
+
+
+ {/* Botón Hamburguesa (Móvil) */}
+
setIsOpen(!isOpen)}
+ className="md:hidden text-navy p-2 outline-none focus:outline-none ring-0"
+ aria-label="Toggle Menu"
+ >
+ {isOpen ? : }
+
+
+
+ {/* Menú Móvil (Overlay) */}
+
+ {isOpen && (
+
+
+ {navLinks.map((link) => (
+
+ {link.name}
+
+ ))}
+
+
+ {/* Selector de Idiomas Móvil */}
+
+ {(["es", "ca", "en"] as const).map((lang) => (
+ setLocale(lang)}
+ className={`text-[16px] font-bold uppercase ${
+ locale === lang ? "text-ocean border-b-2 border-ocean" : "text-navy/40"
+ }`}
+ >
+ {lang === "es" ? "Español" : lang === "ca" ? "Català" : "English"}
+
+ ))}
+
+
+
+
+ {t.navbar.contacto}
+
+
+
+
+
+ )}
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/ui/Badge.tsx b/components/ui/Badge.tsx
new file mode 100644
index 0000000..edadfa9
--- /dev/null
+++ b/components/ui/Badge.tsx
@@ -0,0 +1,26 @@
+"use client";
+import { motion } from "framer-motion";
+import { ReactNode } from "react";
+
+interface BadgeProps {
+ children: ReactNode;
+}
+
+/**
+ * Componente Badge para etiquetas de sección con estilo Flat Premium.
+ *
+ */
+export const Badge = ({ children }: BadgeProps) => {
+ return (
+
+
+ {children}
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/ui/BentoGrid.tsx b/components/ui/BentoGrid.tsx
new file mode 100644
index 0000000..9a4e6e5
--- /dev/null
+++ b/components/ui/BentoGrid.tsx
@@ -0,0 +1,9 @@
+import { ReactNode } from "react";
+
+export const BentoGrid = ({ children, className = "" }: { children: ReactNode; className?: string }) => {
+ return (
+
+ {children}
+
+ );
+};
\ No newline at end of file
diff --git a/components/ui/Button.tsx b/components/ui/Button.tsx
new file mode 100644
index 0000000..e125fd2
--- /dev/null
+++ b/components/ui/Button.tsx
@@ -0,0 +1,44 @@
+"use client";
+import { motion, HTMLMotionProps } from "framer-motion";
+import { ReactNode } from "react";
+
+// Extendemos de HTMLMotionProps para heredar todos los atributos nativos de un botón (incluyendo disabled)
+interface ButtonProps extends HTMLMotionProps<"button"> {
+ children: ReactNode;
+ variant?: "primary" | "outline" | "ocean";
+ className?: string;
+}
+
+export const Button = ({
+ children,
+ variant = "primary",
+ className = "",
+ disabled,
+ ...props
+}: ButtonProps) => {
+
+ const variants = {
+ primary: "bg-navy text-white hover:bg-ocean disabled:bg-navy/50",
+ outline: "border-2 border-navy text-navy hover:bg-navy hover:text-white disabled:opacity-50",
+ ocean: "bg-ocean text-white hover:bg-sky disabled:bg-ocean/50",
+ };
+
+ return (
+
+ {children}
+
+ );
+};
\ No newline at end of file
diff --git a/components/ui/Card.tsx b/components/ui/Card.tsx
new file mode 100644
index 0000000..2dfee08
--- /dev/null
+++ b/components/ui/Card.tsx
@@ -0,0 +1,49 @@
+"use client";
+import { motion } from "framer-motion";
+import { ReactNode } from "react";
+import { twMerge } from "tailwind-merge";
+import { clsx, type ClassValue } from "clsx";
+
+// Utilidad para unir clases de Tailwind
+function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
+
+interface CardProps {
+ children: ReactNode;
+ className?: string;
+ delay?: number;
+ variant?: "white" | "ice" | "navy" | "ocean" | "none";
+}
+
+export const Card = ({
+ children,
+ className = "",
+ delay = 0,
+ variant = "white"
+}: CardProps) => {
+
+ const variants = {
+ white: "bg-white border-crisp hover:border-sky",
+ ice: "bg-ice/20 border-crisp hover:bg-ice/40",
+ navy: "bg-navy border-navy text-white",
+ ocean: "bg-ocean border-ocean text-white",
+ none: "border-crisp"
+ };
+
+ return (
+
+ {children}
+
+ );
+};
\ No newline at end of file
diff --git a/components/ui/Checkbox.tsx b/components/ui/Checkbox.tsx
new file mode 100644
index 0000000..e343e9b
--- /dev/null
+++ b/components/ui/Checkbox.tsx
@@ -0,0 +1,36 @@
+"use client";
+import React, { forwardRef } from "react";
+
+interface CheckboxProps extends React.InputHTMLAttributes {
+ label: string;
+ error?: string;
+}
+
+export const Checkbox = forwardRef(
+ ({ label, error, className = "", ...props }, ref) => {
+ return (
+
+
+
+
+ {label}
+
+
+ {error && (
+
{error}
+ )}
+
+ );
+ }
+);
+
+Checkbox.displayName = "Checkbox";
\ No newline at end of file
diff --git a/components/ui/Input.tsx b/components/ui/Input.tsx
new file mode 100644
index 0000000..861530a
--- /dev/null
+++ b/components/ui/Input.tsx
@@ -0,0 +1,38 @@
+"use client";
+import * as React from "react";
+import { cn } from "@/lib/utils"; // Ahora sí existe
+
+interface InputProps extends React.InputHTMLAttributes {
+ label?: string;
+ error?: string;
+}
+
+export const Input = React.forwardRef(
+ ({ className, label, error, type, ...props }, ref) => {
+ return (
+
+ {label && (
+
+ {label}
+
+ )}
+
+ {error && {error} }
+
+ );
+ }
+);
+Input.displayName = "Input";
\ No newline at end of file
diff --git a/components/ui/Textarea.tsx b/components/ui/Textarea.tsx
new file mode 100644
index 0000000..afc983e
--- /dev/null
+++ b/components/ui/Textarea.tsx
@@ -0,0 +1,34 @@
+"use client";
+import React, { forwardRef } from "react";
+
+interface TextareaProps extends React.TextareaHTMLAttributes {
+ label: string;
+ error?: string;
+}
+
+export const Textarea = forwardRef(
+ ({ label, error, className = "", ...props }, ref) => {
+ return (
+
+
+ {label}
+
+
+ {error && {error} }
+
+ );
+ }
+);
+
+Textarea.displayName = "Textarea";
\ No newline at end of file
diff --git a/context/LanguageContext.tsx b/context/LanguageContext.tsx
new file mode 100644
index 0000000..014c42b
--- /dev/null
+++ b/context/LanguageContext.tsx
@@ -0,0 +1,48 @@
+"use client";
+import React, { createContext, useContext, useState, useEffect } from "react";
+import { es } from "@/lang/es";
+import { ca } from "@/lang/ca";
+import { en } from "@/lang/en";
+
+type Language = "es" | "ca" | "en";
+
+const dictionaries = { es, ca, en };
+
+interface LanguageContextType {
+ locale: Language;
+ setLocale: (lang: Language) => void;
+ t: typeof es;
+}
+
+const LanguageContext = createContext(undefined);
+
+export const LanguageProvider = ({ children }: { children: React.ReactNode }) => {
+ const [locale, setLocaleState] = useState("es");
+
+
+ useEffect(() => {
+ const savedLang = localStorage.getItem("language") as Language;
+ if (savedLang && (savedLang === "es" || savedLang === "ca" || savedLang === "en")) {
+ setLocaleState(savedLang);
+ }
+ }, []);
+
+ const setLocale = (lang: Language) => {
+ setLocaleState(lang);
+ localStorage.setItem("language", lang);
+ };
+
+ const t = dictionaries[locale];
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useLanguage = () => {
+ const context = useContext(LanguageContext);
+ if (!context) throw new Error("useLanguage debe usarse dentro de un LanguageProvider");
+ return context;
+};
\ No newline at end of file
diff --git a/hooks/useForm.ts b/hooks/useForm.ts
new file mode 100644
index 0000000..1085698
--- /dev/null
+++ b/hooks/useForm.ts
@@ -0,0 +1,30 @@
+"use client";
+import { useState } from 'react';
+import { supabase } from '@/lib/supabase/client';
+
+export const useFormSubmit = (tableName: string) => {
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [success, setSuccess] = useState(false);
+ const [error, setError] = useState(null);
+
+ const submit = async (data: any) => {
+ setIsSubmitting(true);
+ setError(null);
+
+ try {
+ const { error: sbError } = await supabase
+ .from(tableName)
+ .insert([data]);
+
+ if (sbError) throw sbError;
+
+ setSuccess(true);
+ } catch (err: any) {
+ setError(err.message || 'Error al enviar los datos');
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return { submit, isSubmitting, success, error };
+};
\ No newline at end of file
diff --git a/lang/ca.ts b/lang/ca.ts
new file mode 100644
index 0000000..64b1313
--- /dev/null
+++ b/lang/ca.ts
@@ -0,0 +1,140 @@
+export const ca = {
+ navbar: {
+ consultora: "La Consultora",
+ servicios: "Serveis",
+ participa: "Participa",
+ insights: "Insights",
+ contacto: "Iniciar diàleg",
+ },
+ home: {
+ badge: "Intel·ligència de Mercat",
+ title: "El valor de saber.",
+ description: "Estudis de mercat i enquestes amb rigor metodològic. Entenem el teu client perquè la teva empresa prengui decisions amb absoluta certesa.",
+ btnServices: "Veure els nostres serveis",
+ btnMethod: "La nostra metodologia",
+ whyUs: {
+ title: "Per què treballar amb nosaltres?",
+ desc: "Combinem la capacitat operativa nacional amb el detall artesanal en l'anàlisi de dades.",
+ card1Title: "Anàlisis especialitzades",
+ card1Desc: "Realitzem tota classe d'estudis i posem en pràctica els nous conceptes i mètodes adaptats al seu sector.",
+ card2Title: "Ampli equip",
+ card2Desc: "La nostra àmplia xarxa cobreix tot el territori nacional i Portugal amb enquestadors supervisats.",
+ card3Title: "Serveis a mida",
+ card3Desc: "Altament flexibles i adaptats. Amb un alt grau de compromís i qualitat analítica.",
+ ctaTitle: "Enlaira't amb dades fiables",
+ ctaDesc: "El nostre equip està llest per estructurar el teu proper estudi d'opinió.",
+ ctaBtn: "Sol·licitar proposta",
+ }
+ },
+ nosotros: {
+ badge: "Trajectòria",
+ title: "Investigació de Mercats. Des de 2004.",
+ desc: "Amb més de 19 anys d'experiència en metodologies qualitatives i quantitatives adaptades al client.",
+ history: {
+ title: "Els nostres inicis i evolució.",
+ p1: "Els nostres inicis es remunten a l'any 2004, quan es funda la Societat Vox Populi Recerca S.L. a Barcelona.",
+ p2: "Després de consolidar la nostra xarxa a Barcelona, ens vam expandir fins a cobrir la totalitat del territori nacional i Portugal.",
+ p3: "Comptem amb els millors tècnics i analistes per donar resposta a totes les tipologies d'estudis.",
+ },
+ mission: {
+ m1Title: "Enfocament al client",
+ m1Desc: "Conèixer les necessitats específiques del mercat per a l'èxit de la seva empresa.",
+ m2Title: "Metodologia adaptada",
+ m2Desc: "Integració de mètodes qualitatius i quantitatius amb màxima precisió.",
+ m3Title: "Compromís transversal",
+ m3Desc: "Aportem visió detallada i certera en projectes de qualsevol envergadura.",
+ }
+ },
+ servicios: {
+ heroBadge: "Cartera de Serveis",
+ hero: "La nostra àmplia cartera de serveis l'ajudarà amb la presa de decisions i a assegurar l'èxit de la seva empresa.",
+ cat1: "Evolució metodològica",
+ cat1Desc: "Realitzem estudis de mercat utilitzant les metodologies quantitatives i qualitatives clàssiques d'investigació i així mateix, ens trobem en constant procés d'evolució.",
+ cat2: "Serveis a mida",
+ cat2Desc: "Els nostres serveis són flexibles i amb metodologia personalitzada. Amb un alt grau de compromís, qualitat i competitivitat. La nostra xarxa cobreix tot el territori nacional.",
+ items: {
+ consultoria: "Consultoria",
+ red: "Xarxa de camp",
+ estudios: "Estudis",
+ },
+ consultoriaDesc: "Volem conèixer les seves necessitats i producte per donar l'enfocament més beneficiós per a la seva empresa.",
+ redDesc: "Un personal altament implicat i qualificat en els estudis, és una de les nostres principals diferències.",
+ estudiosDesc: "Realitzem tota classe d'estudis i posem en pràctica els nous conceptes i mètodes d'investigació.",
+ btnGo: "Anar a la pàgina",
+ },
+ insights: {
+ badge: "Coneixement",
+ title: "Insights & Perspectives.",
+ description: "Anàlisis profundes sobre el mercat ibèric, tendències de consum i metodologies avançades d'investigació.",
+ readMore: "Llegir anàlisi",
+ categories: {
+ tendencias: "Tendències",
+ innovacion: "Innovació",
+ estrategia: "Estratègia"
+ }
+ },
+ participa: {
+ badge: "Xarxa de Col·laboradors",
+ heroTitle: "La teva opinió té valor.",
+ heroDesc: "Uneix-te al nostre panell d'informadors i participa en estudis de mercat, enquestes d'opinió i projectes de Mistery Shopping remunerats.",
+ benefits: {
+ b1Title: "Compensació justa",
+ b1Desc: "Rep incentius econòmics o targetes regal per cada estudi completat.",
+ b2Title: "Flexibilitat total",
+ b2Desc: "Participa online, per telèfon o presencialment segons la teva disponibilitat.",
+ b3Title: "Privadesa garantida",
+ b3Desc: "Les teves dades són anonimitzades. Complim estrictament amb la normativa LOPD i RGPD.",
+ },
+ formTitle: "Completa el teu perfil per començar",
+ fields: {
+ nombre: "Nom",
+ apellidos: "Cognoms",
+ email: "Email",
+ telefono: "Telèfon",
+ provincia: "Província",
+ edad: "Edat",
+ interes: "Interès principal",
+ mensaje: "Missatge (Opcional)"
+ },
+ options: {
+ online: "Enquestes Online (Remot)",
+ telefonicas: "Enquestes Telefòniques",
+ presenciales: "Estudis Presencials (Focus Groups)",
+ mistery: "Mistery Shopping (Client Misteriós)"
+ },
+ success: {
+ title: "Gràcies per unir-te!",
+ desc: "Hem rebut la teva sol·licitud correctament. Ens posarem en contacte amb tu aviat.",
+ btn: "Enviar una altra sol·licitud"
+ }
+ },
+ contacto: {
+ badge: "Contacte Directe",
+ heroTitle: "Iniciem el diàleg.",
+ heroDesc: "Si vols saber més sobre els nostres serveis, sol·licitar pressupost o simplement posar-te en contacte amb nosaltres, omple el formulari.",
+ info: {
+ address: "Adreça",
+ email: "Email",
+ phone: "Telèfon",
+ },
+ form: {
+ title: "Envia'ns la teva consulta",
+ name: "Nom",
+ email: "Email",
+ message: "Dubtes i/o comentaris",
+ privacy: "Accepto la política de privadesa i el tractament de les meves dades per a la gestió d'aquesta sol·licitud.",
+ submit: "Enviar missatge",
+ },
+ success: {
+ title: "Missatge enviat!",
+ desc: "Et respondrem en la major brevetat possible."
+ }
+ },
+ footer: {
+ legal: "Avís Legal",
+ privacy: "Privadesa",
+ cookies: "Cookies",
+ contact: "Contacte",
+ rights: "Tots els drets reservats © 2026 per ITEM OPINA S.L"
+ }
+};
\ No newline at end of file
diff --git a/lang/en.ts b/lang/en.ts
new file mode 100644
index 0000000..996e658
--- /dev/null
+++ b/lang/en.ts
@@ -0,0 +1,140 @@
+export const en = {
+ navbar: {
+ consultora: "The Consultancy",
+ servicios: "Services",
+ participa: "Join Us",
+ insights: "Insights",
+ contacto: "Start Dialogue",
+ },
+ home: {
+ badge: "Market Intelligence",
+ title: "The value of knowing.",
+ description: "Market research and surveys with methodological rigor. We understand your customer so your company can make decisions with absolute certainty.",
+ btnServices: "View our services",
+ btnMethod: "Our methodology",
+ whyUs: {
+ title: "Why work with us?",
+ desc: "We combine national operational capacity with artisanal detail in data analysis.",
+ card1Title: "Specialized Analysis",
+ card1Desc: "We conduct all kinds of studies and implement new concepts and methods adapted to your sector.",
+ card2Title: "Extensive Team",
+ card2Desc: "Our wide network covers the entire national territory and Portugal with supervised interviewers.",
+ card3Title: "Tailored Services",
+ card3Desc: "Highly flexible and adapted. With a high degree of commitment and analytical quality.",
+ ctaTitle: "Take off with reliable data",
+ ctaDesc: "Our team is ready to structure your next opinion study.",
+ ctaBtn: "Request proposal",
+ }
+ },
+ nosotros: {
+ badge: "Trajectory",
+ title: "Market Research. Since 2004.",
+ desc: "With more than 19 years of experience in qualitative and quantitative methodologies adapted to the client.",
+ history: {
+ title: "Our beginnings and evolution.",
+ p1: "Our beginnings date back to 2004, when the Society Vox Populi Recerca S.L. was founded in Barcelona.",
+ p2: "After consolidating our network in Barcelona, we expanded to cover the entire national territory and Portugal.",
+ p3: "We have the best technicians and analysts to respond to all types of studies.",
+ },
+ mission: {
+ m1Title: "Customer Focus",
+ m1Desc: "Understanding specific market needs for your company's success.",
+ m2Title: "Adapted Methodology",
+ m2Desc: "Integration of qualitative and quantitative methods with maximum precision.",
+ m3Title: "Transversal Commitment",
+ m3Desc: "We provide a detailed and accurate vision in projects of any size.",
+ }
+ },
+ servicios: {
+ heroBadge: "Service Portfolio",
+ hero: "Our wide portfolio of services will help you with decision making and ensure your company's success.",
+ cat1: "Methodological Evolution",
+ cat1Desc: "We conduct market research using classic quantitative and qualitative research methodologies and analysis of new methods.",
+ cat2: "Tailored Services",
+ cat2Desc: "Our services are flexible and with a personalized methodology. With a high degree of commitment, quality and competitiveness.",
+ items: {
+ consultoria: "Consultancy",
+ red: "Fieldwork Network",
+ estudios: "Studies",
+ },
+ consultoriaDesc: "We want to know your needs and product to give the most beneficial approach for your company.",
+ redDesc: "Highly involved and qualified staff in studies is one of our main differences.",
+ estudiosDesc: "We carry out all kinds of studies and put into practice new concepts and research methods.",
+ btnGo: "Go to page",
+ },
+ insights: {
+ badge: "Knowledge",
+ title: "Insights & Perspectives.",
+ description: "In-depth analysis of the Iberian market, consumer trends and advanced research methodologies.",
+ readMore: "Read analysis",
+ categories: {
+ tendencias: "Trends",
+ innovacion: "Innovation",
+ estrategia: "Strategy"
+ }
+ },
+ participa: {
+ badge: "Contributors Network",
+ heroTitle: "Your opinion matters.",
+ heroDesc: "Join our panel of informants and participate in market studies, opinion polls and Mystery Shopping projects.",
+ benefits: {
+ b1Title: "Fair Compensation",
+ b1Desc: "Receive financial incentives or gift cards for each completed study.",
+ b2Title: "Total Flexibility",
+ b2Desc: "Participate online, by phone or in person according to your availability.",
+ b3Title: "Guaranteed Privacy",
+ b3Desc: "Your data is anonymized. We strictly comply with GDPR regulations.",
+ },
+ formTitle: "Complete your profile to start",
+ fields: {
+ nombre: "First Name",
+ apellidos: "Last Name",
+ email: "Email",
+ telefono: "Phone",
+ provincia: "Province",
+ edad: "Age",
+ interes: "Main Interest",
+ mensaje: "Message (Optional)"
+ },
+ options: {
+ online: "Online Surveys (Remote)",
+ telefonicas: "Phone Surveys",
+ presenciales: "In-person Studies (Focus Groups)",
+ mistery: "Mystery Shopping (Secret Shopper)"
+ },
+ success: {
+ title: "Thank you for joining!",
+ desc: "We have received your application correctly. We will contact you soon.",
+ btn: "Send another application"
+ }
+ },
+ contacto: {
+ badge: "Direct Contact",
+ heroTitle: "Let's start the dialogue.",
+ heroDesc: "If you want to know more about our services or simply get in touch with us, fill out the form.",
+ info: {
+ address: "Address",
+ email: "Email",
+ phone: "Phone",
+ },
+ form: {
+ title: "Send your inquiry",
+ name: "Name",
+ email: "Email",
+ message: "Questions and/or comments",
+ privacy: "I accept the privacy policy and the processing of my data for the management of this request.",
+ submit: "Send message",
+ },
+ success: {
+ title: "Message sent!",
+ desc: "We will respond to you as soon as possible."
+ }
+ },
+ footer: {
+ legal: "Legal Notice",
+ privacy: "Privacy Policy",
+ cookies: "Cookies",
+ contact: "Contact",
+ rights: "All rights reserved © 2026 by ITEM OPINA S.L",
+ }
+};
\ No newline at end of file
diff --git a/lang/es.ts b/lang/es.ts
new file mode 100644
index 0000000..65f439c
--- /dev/null
+++ b/lang/es.ts
@@ -0,0 +1,140 @@
+export const es = {
+ navbar: {
+ consultora: "La Consultora",
+ servicios: "Servicios",
+ participa: "Participa",
+ insights: "Insights",
+ contacto: "Iniciar diálogo",
+ },
+ home: {
+ badge: "Inteligencia de Mercado",
+ title: "El valor de saber.",
+ description: "Estudios de mercado y encuestas con rigor metodológico. Entendemos a tu cliente para que tu empresa tome decisiones con absoluta certeza.",
+ btnServices: "Ver nuestros servicios",
+ btnMethod: "Nuestra metodología",
+ whyUs: {
+ title: "¿Por qué trabajar con nosotros?",
+ desc: "Combinamos la capacidad operativa nacional con el detalle artesanal en el análisis de datos.",
+ card1Title: "Análisis especializados",
+ card1Desc: "Realizamos toda clase de estudios y ponemos en práctica los nuevos conceptos y métodos adaptados a su sector.",
+ card2Title: "Amplio equipo",
+ card2Desc: "Nuestra amplia red cubre todo el territorio nacional y Portugal con encuestadores supervisados.",
+ card3Title: "Servicios a medida",
+ card3Desc: "Altamente flexibles y adaptados. Con un alto grado de compromiso y calidad analítica.",
+ ctaTitle: "Despega con datos fiables",
+ ctaDesc: "Nuestro equipo está listo para estructurar tu próximo estudio de opinión.",
+ ctaBtn: "Solicitar propuesta",
+ }
+ },
+ nosotros: {
+ badge: "Trayectoria",
+ title: "Investigación de Mercados. Desde 2004.",
+ desc: "Con más de 19 años de experiencia en metodologías cualitativas y cuantitativas adaptadas al cliente.",
+ history: {
+ title: "Nuestros inicios y evolución.",
+ p1: "Nuestros inicios se remontan al año 2004, cuando se funda la Sociedad Vox Populi Recerca S.L. en Barcelona.",
+ p2: "Tras consolidar nuestra red en Barcelona, nos expandimos hasta cubrir la totalidad del territorio nacional y Portugal.",
+ p3: "Contamos con los mejores técnicos y analistas para dar respuesta a todas las tipologías de estudios.",
+ },
+ mission: {
+ m1Title: "Enfoque al cliente",
+ m1Desc: "Conocer las necesidades específicas del mercado para el éxito de su empresa.",
+ m2Title: "Metodología adaptada",
+ m2Desc: "Integración de métodos cualitativos y cuantitativos con máxima precisión.",
+ m3Title: "Compromiso transversal",
+ m3Desc: "Aportamos visión detallada y certera en proyectos de cualquier envergadura.",
+ }
+ },
+ servicios: {
+ heroBadge: "Cartera de Servicios",
+ hero: "Nuestra amplia cartera de servicios le ayudará con la toma de decisiones y a asegurar el éxito de su empresa.",
+ cat1: "Evolución metodológica",
+ cat1Desc: "Realizamos estudios de mercado utilizando las metodologías cuantitativas y cualitativas clásicas de investigación y así mismo, nos encontramos en constante proceso de evolución y análisis de nuevos métodos o adaptándolos a las nuevas visiones del mercado.",
+ cat2: "Servicios a medida",
+ cat2Desc: "Nuestros servicios son flexibles y con metodología personalizada. Con un alto grado de compromiso, calidad y competitividad. Nuestra amplia red de campo cubre Portugal y todo el territorio nacional.",
+ items: {
+ consultoria: "Consultoría",
+ red: "Red de campo",
+ estudios: "Estudios",
+ },
+ consultoriaDesc: "Queremos conocer sus necesidades y producto para dar el enfoque más beneficioso para su empresa.",
+ redDesc: "Un personal altamente implicado y cualificado en los estudios, es una de nuestras principales diferencias.",
+ estudiosDesc: "Realizamos toda clase de estudios y ponemos en práctica los nuevos conceptos y métodos de investigación.",
+ btnGo: "Ir a la página",
+ },
+ insights: {
+ badge: "Conocimiento",
+ title: "Insights & Perspectivas.",
+ description: "Análisis profundos sobre el mercado ibérico, tendencias de consumo y metodologías avanzadas de investigación.",
+ readMore: "Leer análisis",
+ categories: {
+ tendencias: "Tendencias",
+ innovacion: "Innovación",
+ estrategia: "Estrategia"
+ }
+ },
+ participa: {
+ badge: "Red de Colaboradores",
+ heroTitle: "Tu opinión tiene valor.",
+ heroDesc: "Únete a nuestro panel de informadores y participa en estudios de mercado, encuestas de opinión y proyectos de Mistery Shopping remunerados.",
+ benefits: {
+ b1Title: "Compensación justa",
+ b1Desc: "Recibe incentivos económicos o tarjetas regalo por cada estudio completado.",
+ b2Title: "Flexibilidad total",
+ b2Desc: "Participa online, por teléfono o presencialmente según tu disponibilidad.",
+ b3Title: "Privacidad garantizada",
+ b3Desc: "Tus datos son anonimizados. Cumplimos estrictamente con la normativa LOPD y RGPD.",
+ },
+ formTitle: "Completa tu perfil para empezar",
+ fields: {
+ nombre: "Nombre",
+ apellidos: "Apellidos",
+ email: "Email",
+ telefono: "Teléfono",
+ provincia: "Provincia",
+ edad: "Edad",
+ interes: "Interés principal",
+ mensaje: "Mensaje (Opcional)"
+ },
+ options: {
+ online: "Encuestas Online (Remoto)",
+ telefonicas: "Encuestas Telefónicas",
+ presenciales: "Estudios Presenciales (Focus Groups)",
+ mistery: "Mistery Shopping (Cliente Misterioso)"
+ },
+ success: {
+ title: "¡Gracias por unirte!",
+ desc: "Hemos recibido tu solicitud correctamente. Nos pondremos en contacto contigo pronto.",
+ btn: "Enviar otra solicitud"
+ }
+ },
+ contacto: {
+ badge: "Contacto Directo",
+ heroTitle: "Iniciemos el diálogo.",
+ heroDesc: "Si quieres saber más acerca de nuestros servicios, solicitar presupuesto o simplemente ponerte en contacto con nosotros, rellena el formulario.",
+ info: {
+ address: "Dirección",
+ email: "Email",
+ phone: "Teléfono",
+ },
+ form: {
+ title: "Envíanos tu consulta",
+ name: "Nombre",
+ email: "Email",
+ message: "Dudas y/o comentarios",
+ privacy: "Acepto la política de privacidad y el tratamiento de mis datos para la gestión de esta solicitud.",
+ submit: "Enviar mensaje",
+ },
+ success: {
+ title: "¡Mensaje enviado!",
+ desc: "Le responderemos en la mayor brevedad posible."
+ }
+ },
+ footer: {
+ legal: "Aviso Legal",
+ privacy: "Privacidad",
+ cookies: "Cookies",
+ contact: "Contacto",
+ rights: "Todos los derechos reservados © 2026 por ITEM OPINA S.L",
+ }
+};
\ No newline at end of file
diff --git a/lang/index.ts b/lang/index.ts
new file mode 100644
index 0000000..317be38
--- /dev/null
+++ b/lang/index.ts
@@ -0,0 +1,6 @@
+import { es } from './es';
+import { ca } from './ca';
+import { en } from './en';
+
+export const dictionaries = { es, ca, en };
+export type Locale = keyof typeof dictionaries;
\ No newline at end of file
diff --git a/lib/supabase/client.ts b/lib/supabase/client.ts
new file mode 100644
index 0000000..e01b233
--- /dev/null
+++ b/lib/supabase/client.ts
@@ -0,0 +1,13 @@
+import { createClient } from '@supabase/supabase-js';
+
+const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
+const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY;
+
+if (!supabaseUrl || !supabaseKey) {
+ console.warn("⚠️ Supabase: Faltan las variables de entorno en .env.local");
+}
+
+export const supabase = createClient(
+ supabaseUrl || 'https://placeholder.supabase.co',
+ supabaseKey || 'placeholder'
+);
\ No newline at end of file
diff --git a/lib/supabase/middleware.ts b/lib/supabase/middleware.ts
new file mode 100644
index 0000000..e69de29
diff --git a/lib/supabase/server.ts b/lib/supabase/server.ts
new file mode 100644
index 0000000..e69de29
diff --git a/lib/utils.ts b/lib/utils.ts
new file mode 100644
index 0000000..daab5de
--- /dev/null
+++ b/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
\ No newline at end of file
diff --git a/next.config.ts b/next.config.ts
index e9ffa30..03bdd1e 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,7 +1,15 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
- /* config options here */
+ images: {
+ remotePatterns: [
+ {
+ protocol: 'https',
+ hostname: 'images.unsplash.com',
+ pathname: '**',
+ },
+ ],
+ },
};
-export default nextConfig;
+export default nextConfig;
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 9b3c5e2..d2bfc72 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,9 +8,18 @@
"name": "item_opina_frontend",
"version": "0.1.0",
"dependencies": {
+ "@hookform/resolvers": "^5.2.2",
+ "@supabase/supabase-js": "^2.105.4",
+ "clsx": "^2.1.1",
+ "framer-motion": "^12.38.0",
+ "lucide-react": "^1.14.0",
"next": "16.2.6",
"react": "19.2.4",
- "react-dom": "19.2.4"
+ "react-dom": "19.2.4",
+ "react-hook-form": "^7.75.0",
+ "react-icons": "^5.6.0",
+ "tailwind-merge": "^3.5.0",
+ "zod": "^4.4.3"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@@ -453,6 +462,18 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@hookform/resolvers": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz",
+ "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/utils": "^0.3.0"
+ },
+ "peerDependencies": {
+ "react-hook-form": "^7.55.0"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
@@ -1307,6 +1328,96 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@standard-schema/utils": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
+ "license": "MIT"
+ },
+ "node_modules/@supabase/auth-js": {
+ "version": "2.105.4",
+ "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.105.4.tgz",
+ "integrity": "sha512-Ejfa37M5xoIwoxVebxRahnwubPo8g22qkXQ4p50+N9MIvU9UZoN+A8dwVPtczzGf8oV/YXN80ZPxK4aWXuSN/A==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/functions-js": {
+ "version": "2.105.4",
+ "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.105.4.tgz",
+ "integrity": "sha512-JVNKbBft3Qkja+WlGaE026AJ2AH9K0UTsxsfvEIHgd4zFrBor4BYRCrYFrv9IDsvVqkF72wKDsODJl5GY/C4tA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/phoenix": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.2.tgz",
+ "integrity": "sha512-YSAGnmDAfuleFCVt3CeurQZAhxRfXWeZIIkwp7NhYzQ1UwW6ePSnzsFAiUm/mbCkfoCf70QQHKW/K6RKh52a4A==",
+ "license": "MIT"
+ },
+ "node_modules/@supabase/postgrest-js": {
+ "version": "2.105.4",
+ "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.105.4.tgz",
+ "integrity": "sha512-SppIyLo/kTwIlz1qpv2HN1EQqBg0GVktrDDFsXygYROha3MgVn4rT7p5EjFHFqXQm2rdRGb/BI7bc+jr10m91w==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/realtime-js": {
+ "version": "2.105.4",
+ "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.105.4.tgz",
+ "integrity": "sha512-6ov6c59+8D9h7q4M4Gy/uDJlC0Akxl9/714Y+6vJ+Sijuc16TS/p5DwhfRCLNcIhNiej1gEt+CQUwsjiPt4PxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/phoenix": "^0.4.2",
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/storage-js": {
+ "version": "2.105.4",
+ "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.105.4.tgz",
+ "integrity": "sha512-Jx+pzMP1Whjof2PWHoVBUA75/p7PQE9CqKBzn1oXVyJDOggMLSH2OzVWwsXYaxEpdC1K/KltwmOX44nL3LHl9g==",
+ "license": "MIT",
+ "dependencies": {
+ "iceberg-js": "^0.8.1",
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/supabase-js": {
+ "version": "2.105.4",
+ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.105.4.tgz",
+ "integrity": "sha512-cEnx+k49knU+qdIP7rXwR6fqEXPHZs+74xFK1R0S8MgQ7v9tbePVdGxvO03n3bPympMdJWVLadARBfU4TgNHCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/auth-js": "2.105.4",
+ "@supabase/functions-js": "2.105.4",
+ "@supabase/postgrest-js": "2.105.4",
+ "@supabase/realtime-js": "2.105.4",
+ "@supabase/storage-js": "2.105.4"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -2715,6 +2826,15 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -3706,6 +3826,33 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/framer-motion": {
+ "version": "12.38.0",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
+ "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.38.0",
+ "motion-utils": "^12.36.0",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -4011,6 +4158,15 @@
"hermes-estree": "0.25.1"
}
},
+ "node_modules/iceberg-js": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
+ "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -4966,6 +5122,15 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react": {
+ "version": "1.14.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz",
+ "integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -5033,6 +5198,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/motion-dom": {
+ "version": "12.38.0",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
+ "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.36.0"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.36.0",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz",
+ "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==",
+ "license": "MIT"
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -5551,6 +5731,31 @@
"react": "^19.2.4"
}
},
+ "node_modules/react-hook-form": {
+ "version": "7.75.0",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.75.0.tgz",
+ "integrity": "sha512-Ovv94H+0p3sJ7B9B5QxPuCP1u8V/cHuVGyH55cSwodYDtoJwK+fqk3vjfIgSX59I2U/bU4z0nRJ9HMLpNiWEmw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18 || ^19"
+ }
+ },
+ "node_modules/react-icons": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz",
+ "integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -6173,6 +6378,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/tailwind-merge": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz",
+ "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
"node_modules/tailwindcss": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz",
@@ -6670,7 +6885,6 @@
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
- "dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
diff --git a/package.json b/package.json
index 56d11bb..42289f1 100644
--- a/package.json
+++ b/package.json
@@ -2,6 +2,9 @@
"name": "item_opina_frontend",
"version": "0.1.0",
"private": true,
+ "engines": {
+ "node": ">=20"
+ },
"scripts": {
"dev": "next dev",
"build": "next build",
@@ -9,9 +12,18 @@
"lint": "eslint"
},
"dependencies": {
+ "@hookform/resolvers": "^5.2.2",
+ "@supabase/supabase-js": "^2.105.4",
+ "clsx": "^2.1.1",
+ "framer-motion": "^12.38.0",
+ "lucide-react": "^1.14.0",
"next": "16.2.6",
"react": "19.2.4",
- "react-dom": "19.2.4"
+ "react-dom": "19.2.4",
+ "react-hook-form": "^7.75.0",
+ "react-icons": "^5.6.0",
+ "tailwind-merge": "^3.5.0",
+ "zod": "^4.4.3"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
diff --git a/structure.sh b/structure.sh
new file mode 100644
index 0000000..71c53f0
--- /dev/null
+++ b/structure.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+
+# Crear estructura de carpetas principal dentro de app/
+mkdir -p app/nosotros
+mkdir -p app/servicios
+mkdir -p app/participa
+mkdir -p app/contacto
+
+# Crear carpetas para componentes, hooks, tipos y utilidades
+mkdir -p components/ui
+mkdir -p components/layout
+mkdir -p components/forms
+mkdir -p components/sections
+mkdir -p hooks
+mkdir -p lib/supabase
+mkdir -p types
+mkdir -p utils
+
+# --- Crear Archivos de Rutas ---
+touch app/nosotros/page.tsx
+touch app/servicios/page.tsx
+touch app/participa/page.tsx
+touch app/contacto/page.tsx
+
+# --- Crear Componentes de Layout ---
+touch components/layout/Navbar.tsx
+touch components/layout/Footer.tsx
+
+# --- Crear Componentes de UI (Base Flat Premium) ---
+touch components/ui/Button.tsx
+touch components/ui/Input.tsx
+touch components/ui/Textarea.tsx
+touch components/ui/Checkbox.tsx
+touch components/ui/Card.tsx
+touch components/ui/BentoGrid.tsx
+
+# --- Crear Componentes de Formularios (Con lógica de validación) ---
+touch components/forms/ParticipaForm.tsx
+touch components/forms/ContactoForm.tsx
+
+# --- Crear Configuración de Supabase ---
+touch lib/supabase/client.ts
+touch lib/supabase/server.ts
+touch lib/supabase/middleware.ts
+
+# --- Crear Archivos de Apoyo ---
+touch types/index.ts
+touch utils/validations.ts
+touch hooks/useForm.ts
+
+echo "Estructura de ITEM OPINA creada con éxito."
\ No newline at end of file
diff --git a/supabase/.temp/linked-project.json b/supabase/.temp/linked-project.json
new file mode 100644
index 0000000..bedb2c1
--- /dev/null
+++ b/supabase/.temp/linked-project.json
@@ -0,0 +1 @@
+{"ref":"lziefvcawykgpxqjksql","name":"vox-populi-setup","organization_id":"bgxfmcukiozhchitkokr","organization_slug":"bgxfmcukiozhchitkokr"}
\ No newline at end of file
diff --git a/types/index.ts b/types/index.ts
new file mode 100644
index 0000000..e69de29
diff --git a/utils/validations.ts b/utils/validations.ts
new file mode 100644
index 0000000..2574f00
--- /dev/null
+++ b/utils/validations.ts
@@ -0,0 +1,38 @@
+import { z } from "zod";
+
+// --- ESQUEMA DE CONTACTO ---
+export const ContactoSchema = z.object({
+ nombre: z.string().min(2, "El nombre es demasiado corto"),
+ email: z.string().email("Introduce un correo electrónico válido"),
+ mensaje: z.string().min(10, "Cuéntanos un poco más sobre tu proyecto"),
+ privacidad: z.boolean().refine((val) => val === true, {
+ message: "Debes aceptar la política de privacidad",
+ }),
+});
+
+export type ContactoFormData = z.infer;
+
+
+// --- ESQUEMA DE PARTICIPACIÓN ---
+export const ParticipaSchema = z.object({
+ nombre: z.string().min(2, "El nombre es demasiado corto"),
+ apellidos: z.string().min(2, "Los apellidos son necesarios"),
+ email: z.string().email("Introduce un correo electrónico válido"),
+ telefono: z.string().min(9, "El teléfono no es válido"),
+ provincia: z.string().min(2, "Indica tu provincia"),
+ edad: z.string().refine((val) => !isNaN(Number(val)) && Number(val) > 0, {
+ message: "Introduce una edad válida",
+ }),
+
+ // SOLUCIÓN: Ahora acepta un string genérico (el ID de Supabase) o vacío (opcional)
+ interes: z.string().optional(),
+
+ mensaje: z.string().optional(),
+
+ // Usar boolean().refine() es más seguro con checkboxes en React Hook Form
+ privacidad: z.boolean().refine((val) => val === true, {
+ message: "Debes aceptar la política de privacidad",
+ }),
+});
+
+export type ParticipaFormData = z.infer;
\ No newline at end of file