Configuracion de estilos excelente
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import Navbar from '@/components/Navbar/Navbar';
|
||||
import Hero from '@/components/Hero/Hero';
|
||||
import ReformaSlider from '@/components/ReformaSlider/ReformaSlider';
|
||||
import Features from '@/components/Features/Features';
|
||||
import Pricing from '@/components/Pricing/Pricing';
|
||||
import ContactForm from '@/components/ContactForm/ContactForm';
|
||||
@@ -8,12 +9,12 @@ import Footer from '@/components/Footer/Footer';
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
{/* <Navbar /> */}
|
||||
<main id="main-content">
|
||||
<Hero />
|
||||
<ReformaSlider />
|
||||
<Features />
|
||||
<Pricing />
|
||||
<ContactForm />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
|
||||
@@ -269,11 +269,10 @@ export default function ContactForm() {
|
||||
id="contact-name"
|
||||
name="name"
|
||||
type="text"
|
||||
className={`w-full px-4 py-3 text-base font-sans text-dark bg-white border-[1.5px] rounded-lg transition-all duration-150 outline-none placeholder:text-gray-400 focus:border-black focus:shadow-[0_0_0_3px_rgba(0,0,0,0.06)] ${
|
||||
errors.name && touched.name
|
||||
className={`w-full px-4 py-3 text-base font-sans text-dark bg-white border-[1.5px] rounded-lg transition-all duration-150 outline-none placeholder:text-gray-400 focus:border-black focus:shadow-[0_0_0_3px_rgba(0,0,0,0.06)] ${errors.name && touched.name
|
||||
? 'border-error shadow-[0_0_0_3px_rgba(255,59,59,0.08)]'
|
||||
: 'border-gray-200'
|
||||
}`}
|
||||
}`}
|
||||
placeholder="Juan García"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
@@ -298,11 +297,10 @@ export default function ContactForm() {
|
||||
id="contact-email"
|
||||
name="email"
|
||||
type="email"
|
||||
className={`w-full px-4 py-3 text-base font-sans text-dark bg-white border-[1.5px] rounded-lg transition-all duration-150 outline-none placeholder:text-gray-400 focus:border-black focus:shadow-[0_0_0_3px_rgba(0,0,0,0.06)] ${
|
||||
errors.email && touched.email
|
||||
className={`w-full px-4 py-3 text-base font-sans text-dark bg-white border-[1.5px] rounded-lg transition-all duration-150 outline-none placeholder:text-gray-400 focus:border-black focus:shadow-[0_0_0_3px_rgba(0,0,0,0.06)] ${errors.email && touched.email
|
||||
? 'border-error shadow-[0_0_0_3px_rgba(255,59,59,0.08)]'
|
||||
: 'border-gray-200'
|
||||
}`}
|
||||
}`}
|
||||
placeholder="juan@empresa.com"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
@@ -330,11 +328,10 @@ export default function ContactForm() {
|
||||
id="contact-company"
|
||||
name="company"
|
||||
type="text"
|
||||
className={`w-full px-4 py-3 text-base font-sans text-dark bg-white border-[1.5px] rounded-lg transition-all duration-150 outline-none placeholder:text-gray-400 focus:border-black focus:shadow-[0_0_0_3px_rgba(0,0,0,0.06)] ${
|
||||
errors.company && touched.company
|
||||
className={`w-full px-4 py-3 text-base font-sans text-dark bg-white border-[1.5px] rounded-lg transition-all duration-150 outline-none placeholder:text-gray-400 focus:border-black focus:shadow-[0_0_0_3px_rgba(0,0,0,0.06)] ${errors.company && touched.company
|
||||
? 'border-error shadow-[0_0_0_3px_rgba(255,59,59,0.08)]'
|
||||
: 'border-gray-200'
|
||||
}`}
|
||||
}`}
|
||||
placeholder="Mi Empresa S.A."
|
||||
value={formData.company}
|
||||
onChange={handleChange}
|
||||
@@ -360,11 +357,10 @@ export default function ContactForm() {
|
||||
id="contact-phone"
|
||||
name="phone"
|
||||
type="tel"
|
||||
className={`w-full px-4 py-3 text-base font-sans text-dark bg-white border-[1.5px] rounded-lg transition-all duration-150 outline-none placeholder:text-gray-400 focus:border-black focus:shadow-[0_0_0_3px_rgba(0,0,0,0.06)] ${
|
||||
errors.phone && touched.phone
|
||||
className={`w-full px-4 py-3 text-base font-sans text-dark bg-white border-[1.5px] rounded-lg transition-all duration-150 outline-none placeholder:text-gray-400 focus:border-black focus:shadow-[0_0_0_3px_rgba(0,0,0,0.06)] ${errors.phone && touched.phone
|
||||
? 'border-error shadow-[0_0_0_3px_rgba(255,59,59,0.08)]'
|
||||
: 'border-gray-200'
|
||||
}`}
|
||||
}`}
|
||||
placeholder="+52 55 1234 5678"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
@@ -389,11 +385,10 @@ export default function ContactForm() {
|
||||
<textarea
|
||||
id="contact-message"
|
||||
name="message"
|
||||
className={`w-full px-4 py-3 text-base font-sans text-dark bg-white border-[1.5px] rounded-lg transition-all duration-150 outline-none placeholder:text-gray-400 focus:border-black focus:shadow-[0_0_0_3px_rgba(0,0,0,0.06)] resize-y min-h-[120px] ${
|
||||
errors.message && touched.message
|
||||
className={`w-full px-4 py-3 text-base font-sans text-dark bg-white border-[1.5px] rounded-lg transition-all duration-150 outline-none placeholder:text-gray-400 focus:border-black focus:shadow-[0_0_0_3px_rgba(0,0,0,0.06)] resize-y min-h-[120px] ${errors.message && touched.message
|
||||
? 'border-error shadow-[0_0_0_3px_rgba(255,59,59,0.08)]'
|
||||
: 'border-gray-200'
|
||||
}`}
|
||||
}`}
|
||||
placeholder="Cuéntanos sobre tu proyecto, equipo y qué quieres lograr con FlowSync..."
|
||||
rows={5}
|
||||
value={formData.message}
|
||||
|
||||
@@ -15,10 +15,9 @@ const features = [
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
title: 'Automatización inteligente',
|
||||
description:
|
||||
'Crea flujos de trabajo sin código. Conecta apps, dispara acciones y elimina tareas manuales con nuestra IA incorporada.',
|
||||
tag: 'IA',
|
||||
title: 'Deja tu número',
|
||||
description: 'Rellena tu nombre, teléfono y email. En menos de 2 minutos te llamamos.',
|
||||
tag: '01',
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
@@ -32,10 +31,9 @@ const features = [
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
title: 'Colaboración en tiempo real',
|
||||
description:
|
||||
'Tu equipo siempre sincronizado. Comenta, asigna tareas y sigue el progreso de cada proyecto en tiempo real.',
|
||||
tag: 'Equipos',
|
||||
title: 'Sube una foto',
|
||||
description: 'Fotografía tu cocina o baño. Nuestra IA analiza el espacio.',
|
||||
tag: '02',
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
@@ -49,55 +47,9 @@ const features = [
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
title: 'Analíticas avanzadas',
|
||||
description:
|
||||
'Paneles personalizables con métricas de negocio en tiempo real. Toma decisiones basadas en datos, no en suposiciones.',
|
||||
tag: 'Analytics',
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" stroke="currentColor" strokeWidth="2" />
|
||||
<path d="M7 11V7a5 5 0 0110 0v4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
),
|
||||
title: 'Seguridad enterprise',
|
||||
description:
|
||||
'Cifrado end-to-end, SSO, auditoría de accesos y cumplimiento SOC 2 Tipo II. Tu información, siempre protegida.',
|
||||
tag: 'Seguridad',
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" />
|
||||
<path
|
||||
d="M2 12h20M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
title: '200+ integraciones',
|
||||
description:
|
||||
'Conecta con Slack, Notion, GitHub, Salesforce y más. FlowSync se adapta a tu stack, no al revés.',
|
||||
tag: 'Integraciones',
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
title: 'Soporte 24/7',
|
||||
description:
|
||||
'Equipo de soporte dedicado disponible en cualquier zona horaria. Tiempo de respuesta promedio: menos de 2 minutos.',
|
||||
tag: 'Soporte',
|
||||
title: 'Recibe tu presupuesto',
|
||||
description: 'Al colgar te llega por WhatsApp el render + presupuesto desglosado.',
|
||||
tag: '03',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -160,7 +112,7 @@ export default function Features() {
|
||||
{feature.icon}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-xs font-bold uppercase tracking-widest text-gray-400">
|
||||
<div className="text-[6rem] font-black leading-none text-gray-300 select-none -mt-2 -mb-2">
|
||||
{feature.tag}
|
||||
</div>
|
||||
<h3 className="text-xl font-extrabold tracking-tight text-black leading-tight">
|
||||
@@ -173,20 +125,6 @@ export default function Features() {
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Bottom CTA */}
|
||||
<div className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out text-center mt-16 flex flex-col items-center gap-4">
|
||||
<p className="text-xl font-bold text-black tracking-tight">
|
||||
¿Listo para transformar cómo trabaja tu equipo?
|
||||
</p>
|
||||
<button
|
||||
className="btn btn-primary btn-lg"
|
||||
id="features-cta-btn"
|
||||
onClick={() => document.querySelector('#contact')?.scrollIntoView({ behavior: 'smooth' })}
|
||||
>
|
||||
Ver todas las funciones
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -40,37 +40,6 @@ const socials = [
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="bg-black text-white" role="contentinfo">
|
||||
{/* CTA Banner */}
|
||||
<div className="bg-gray-900 border-b border-white/10">
|
||||
<div className="container">
|
||||
<div className="flex items-center justify-between gap-8 py-12 flex-wrap max-sm:flex-col max-sm:text-center">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2 className="text-[clamp(1.5rem,4vw,2.25rem)] font-black tracking-[-0.04em] text-white">
|
||||
Empieza hoy mismo — es gratis
|
||||
</h2>
|
||||
<p className="text-base text-white/50">
|
||||
Únete a más de 10,000 equipos que ya usan FlowSync
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3 shrink-0 flex-wrap max-sm:w-full max-sm:flex-col">
|
||||
<button
|
||||
className="btn btn-accent btn-lg max-sm:w-full max-sm:justify-center"
|
||||
id="footer-cta-btn"
|
||||
onClick={() => document.querySelector('#contact')?.scrollIntoView({ behavior: 'smooth' })}
|
||||
>
|
||||
Prueba gratis 14 días
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-lg border-white/20 text-white bg-white/10 hover:bg-white/20 hover:border-white/30 max-sm:w-full max-sm:justify-center"
|
||||
id="footer-demo-btn"
|
||||
>
|
||||
Ver demo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main footer */}
|
||||
<div className="py-16 pb-8">
|
||||
<div className="container">
|
||||
|
||||
@@ -1,13 +1,292 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
const stats = [
|
||||
{ value: '10K+', label: 'Equipos activos' },
|
||||
{ value: '99.9%', label: 'Uptime garantizado' },
|
||||
{ value: '3x', label: 'Más productividad' },
|
||||
{ value: '140+', label: 'Países' },
|
||||
];
|
||||
type FormData = {
|
||||
name: string;
|
||||
email: string;
|
||||
company: string;
|
||||
phone: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
type FormErrors = Partial<Record<keyof FormData, string>>;
|
||||
type SubmitStatus = 'idle' | 'loading' | 'success' | 'error';
|
||||
|
||||
const initialData: FormData = {
|
||||
name: '',
|
||||
email: '',
|
||||
company: '',
|
||||
phone: '',
|
||||
message: '',
|
||||
};
|
||||
|
||||
function validateForm(data: FormData): FormErrors {
|
||||
const errors: FormErrors = {};
|
||||
if (!data.name.trim()) errors.name = 'El nombre es requerido';
|
||||
if (!data.email.trim()) {
|
||||
errors.email = 'El email es requerido';
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
|
||||
errors.email = 'Ingresa un email válido';
|
||||
}
|
||||
if (!data.company.trim()) errors.company = 'La empresa es requerida';
|
||||
if (data.phone && !/^[+\d\s\-().]{7,20}$/.test(data.phone)) {
|
||||
errors.phone = 'Ingresa un teléfono válido';
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
function LeadForm() {
|
||||
const [formData, setFormData] = useState<FormData>(initialData);
|
||||
const [errors, setErrors] = useState<FormErrors>({});
|
||||
const [touched, setTouched] = useState<Partial<Record<keyof FormData, boolean>>>({});
|
||||
const [status, setStatus] = useState<SubmitStatus>('idle');
|
||||
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
if (touched[name as keyof FormData]) {
|
||||
const newErrors = validateForm({ ...formData, [name]: value });
|
||||
setErrors((prev) => ({ ...prev, [name]: newErrors[name as keyof FormData] }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = (
|
||||
e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
const { name } = e.target;
|
||||
setTouched((prev) => ({ ...prev, [name]: true }));
|
||||
const newErrors = validateForm(formData);
|
||||
setErrors((prev) => ({ ...prev, [name]: newErrors[name as keyof FormData] }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const allTouched = Object.keys(formData).reduce(
|
||||
(acc, k) => ({ ...acc, [k]: true }),
|
||||
{} as Record<keyof FormData, boolean>
|
||||
);
|
||||
setTouched(allTouched);
|
||||
|
||||
const validationErrors = validateForm(formData);
|
||||
if (Object.keys(validationErrors).length > 0) {
|
||||
setErrors(validationErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus('loading');
|
||||
await new Promise((resolve) => setTimeout(resolve, 1800));
|
||||
setStatus('success');
|
||||
setFormData(initialData);
|
||||
setTouched({});
|
||||
setErrors({});
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setStatus('idle');
|
||||
setFormData(initialData);
|
||||
setErrors({});
|
||||
setTouched({});
|
||||
};
|
||||
|
||||
if (status === 'success') {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col items-center justify-center text-center gap-4 py-16 px-8 animate-scaleIn"
|
||||
role="alert"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div className="w-18 h-18 bg-black text-white rounded-full flex items-center justify-center mb-2 p-4">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M6 16l7 7L26 9"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-2xl font-extrabold tracking-tight text-black">
|
||||
¡Mensaje enviado!
|
||||
</h3>
|
||||
<p className="text-base text-gray-600 max-w-[320px] leading-relaxed mb-4">
|
||||
Gracias por contactarnos. Nuestro equipo te responderá en menos de 24 horas.
|
||||
</p>
|
||||
<button
|
||||
className="btn btn-secondary text-sm px-4 py-2 bg-gray-100 hover:bg-gray-200 text-black font-semibold rounded-lg transition-colors"
|
||||
onClick={handleReset}
|
||||
>
|
||||
Enviar otro mensaje
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
className="flex flex-col gap-5"
|
||||
onSubmit={handleSubmit}
|
||||
noValidate
|
||||
aria-label="Formulario de contacto"
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label htmlFor="lead-name" className="text-sm font-semibold text-dark">
|
||||
Nombre completo <span className="text-error">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="lead-name"
|
||||
name="name"
|
||||
type="text"
|
||||
className={`w-full px-4 py-3 text-base font-sans text-dark bg-white border-[1.5px] rounded-lg transition-all duration-150 outline-none placeholder:text-gray-400 focus:border-black focus:shadow-[0_0_0_3px_rgba(0,0,0,0.06)] ${errors.name && touched.name
|
||||
? 'border-error shadow-[0_0_0_3px_rgba(255,59,59,0.08)]'
|
||||
: 'border-gray-200'
|
||||
}`}
|
||||
placeholder="Juan García"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
autoComplete="name"
|
||||
aria-required="true"
|
||||
aria-describedby={errors.name && touched.name ? 'name-error' : undefined}
|
||||
aria-invalid={!!(errors.name && touched.name)}
|
||||
/>
|
||||
{errors.name && touched.name && (
|
||||
<span id="name-error" className="text-xs text-error font-medium flex items-center gap-1" role="alert">
|
||||
{errors.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label htmlFor="lead-email" className="text-sm font-semibold text-dark">
|
||||
Email <span className="text-error">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="lead-email"
|
||||
name="email"
|
||||
type="email"
|
||||
className={`w-full px-4 py-3 text-base font-sans text-dark bg-white border-[1.5px] rounded-lg transition-all duration-150 outline-none placeholder:text-gray-400 focus:border-black focus:shadow-[0_0_0_3px_rgba(0,0,0,0.06)] ${errors.email && touched.email
|
||||
? 'border-error shadow-[0_0_0_3px_rgba(255,59,59,0.08)]'
|
||||
: 'border-gray-200'
|
||||
}`}
|
||||
placeholder="juan@empresa.com"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
autoComplete="email"
|
||||
aria-required="true"
|
||||
aria-describedby={errors.email && touched.email ? 'email-error' : undefined}
|
||||
aria-invalid={!!(errors.email && touched.email)}
|
||||
/>
|
||||
{errors.email && touched.email && (
|
||||
<span id="email-error" className="text-xs text-error font-medium flex items-center gap-1" role="alert">
|
||||
{errors.email}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label htmlFor="lead-company" className="text-sm font-semibold text-dark">
|
||||
Empresa <span className="text-error">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="lead-company"
|
||||
name="company"
|
||||
type="text"
|
||||
className={`w-full px-4 py-3 text-base font-sans text-dark bg-white border-[1.5px] rounded-lg transition-all duration-150 outline-none placeholder:text-gray-400 focus:border-black focus:shadow-[0_0_0_3px_rgba(0,0,0,0.06)] ${errors.company && touched.company
|
||||
? 'border-error shadow-[0_0_0_3px_rgba(255,59,59,0.08)]'
|
||||
: 'border-gray-200'
|
||||
}`}
|
||||
placeholder="Mi Empresa S.A."
|
||||
value={formData.company}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
autoComplete="organization"
|
||||
aria-required="true"
|
||||
aria-describedby={errors.company && touched.company ? 'company-error' : undefined}
|
||||
aria-invalid={!!(errors.company && touched.company)}
|
||||
/>
|
||||
{errors.company && touched.company && (
|
||||
<span id="company-error" className="text-xs text-error font-medium flex items-center gap-1" role="alert">
|
||||
{errors.company}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label htmlFor="lead-phone" className="text-sm font-semibold text-dark">
|
||||
Teléfono <span className="font-normal text-gray-400">(opcional)</span>
|
||||
</label>
|
||||
<input
|
||||
id="lead-phone"
|
||||
name="phone"
|
||||
type="tel"
|
||||
className={`w-full px-4 py-3 text-base font-sans text-dark bg-white border-[1.5px] rounded-lg transition-all duration-150 outline-none placeholder:text-gray-400 focus:border-black focus:shadow-[0_0_0_3px_rgba(0,0,0,0.06)] ${errors.phone && touched.phone
|
||||
? 'border-error shadow-[0_0_0_3px_rgba(255,59,59,0.08)]'
|
||||
: 'border-gray-200'
|
||||
}`}
|
||||
placeholder="+52 55 1234 5678"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
autoComplete="tel"
|
||||
aria-describedby={errors.phone && touched.phone ? 'phone-error' : undefined}
|
||||
aria-invalid={!!(errors.phone && touched.phone)}
|
||||
/>
|
||||
{errors.phone && touched.phone && (
|
||||
<span id="phone-error" className="text-xs text-error font-medium flex items-center gap-1" role="alert">
|
||||
{errors.phone}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-black text-white py-4 text-sm font-medium rounded-lg transition-opacity disabled:opacity-40 disabled:cursor-not-allowed hover:opacity-90 flex justify-center items-center gap-2 mt-2"
|
||||
disabled={status === 'loading'}
|
||||
aria-busy={status === 'loading'}
|
||||
>
|
||||
{status === 'loading' ? (
|
||||
<>
|
||||
<span
|
||||
className="w-[18px] h-[18px] border-2 border-white/30 border-t-white rounded-full animate-[spin_0.7s_linear_infinite] shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Enviando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Enviar mensaje
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M2 8h12M10 4l4 4-4 4"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<p className="text-xs text-gray-400 text-center leading-relaxed">
|
||||
Al enviar, aceptas nuestra{' '}
|
||||
<a href="#" className="text-gray-600 underline underline-offset-2 hover:text-black">
|
||||
política de privacidad
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Hero() {
|
||||
const heroRef = useRef<HTMLElement>(null);
|
||||
@@ -24,190 +303,107 @@ export default function Hero() {
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
const elements = heroRef.current?.querySelectorAll('.reveal');
|
||||
elements?.forEach((el) => observer.observe(el));
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const handleScrollToContact = () => {
|
||||
document.querySelector('#contact')?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const handleScrollToFeatures = () => {
|
||||
document.querySelector('#features')?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
className="pt-[72px] bg-white overflow-hidden"
|
||||
id="hero"
|
||||
ref={heroRef}
|
||||
aria-label="Sección principal"
|
||||
>
|
||||
<div className="container flex flex-col items-center text-center pt-20 pb-16 gap-6">
|
||||
{/* Badge */}
|
||||
<div className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out delay-100 mb-2">
|
||||
<span className="badge badge-dark">
|
||||
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" aria-hidden="true">
|
||||
<circle cx="4" cy="4" r="4" fill="#00c853" />
|
||||
</svg>
|
||||
Nuevo — Ahora con IA integrada
|
||||
</span>
|
||||
</div>
|
||||
<section className="bg-white overflow-hidden" id="hero" ref={heroRef} aria-label="Sección principal">
|
||||
<div className="container py-16 md:pt-24 pb-8">
|
||||
|
||||
{/* Heading */}
|
||||
<h1 className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out delay-200 text-[clamp(2.5rem,7vw,5rem)] font-black tracking-[-0.04em] leading-[1.05] text-black max-w-[800px]">
|
||||
El flujo de trabajo
|
||||
<br />
|
||||
<em className="italic font-black">que tu equipo</em>
|
||||
<br />
|
||||
siempre necesitó
|
||||
</h1>
|
||||
{/* Grid 2 columnas */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 md:gap-16 items-start">
|
||||
|
||||
{/* Subheading */}
|
||||
<p className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out delay-300 text-[clamp(1rem,2vw,1.25rem)] text-gray-600 max-w-[520px] leading-relaxed mt-2">
|
||||
FlowSync conecta a tu equipo, automatiza tareas repetitivas y te da
|
||||
visibilidad total de cada proyecto — todo en un solo lugar.
|
||||
</p>
|
||||
{/* Columna izquierda — textos */}
|
||||
<div className="flex flex-col gap-6">
|
||||
<h1 className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out text-[clamp(2.5rem,5vw,4rem)] font-black tracking-[-0.04em] leading-[1.05] text-black">
|
||||
Tu reforma,
|
||||
<br />
|
||||
<em className="italic font-black">presupuestada</em>
|
||||
<br />
|
||||
en 5 minutos.
|
||||
</h1>
|
||||
|
||||
{/* CTA Buttons */}
|
||||
<div className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out delay-400 flex flex-wrap justify-center gap-3 mt-2 max-sm:flex-col max-sm:w-full max-sm:max-w-[320px]">
|
||||
<button
|
||||
className="btn btn-primary btn-lg max-sm:w-full"
|
||||
id="hero-cta-primary"
|
||||
onClick={handleScrollToContact}
|
||||
>
|
||||
Empieza gratis
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M3 8h10M9 4l4 4-4 4"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-lg max-sm:w-full"
|
||||
id="hero-cta-secondary"
|
||||
onClick={handleScrollToFeatures}
|
||||
>
|
||||
Ver características
|
||||
</button>
|
||||
</div>
|
||||
<p className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out delay-100 text-lg text-gray-500 leading-relaxed max-w-md">
|
||||
Deja tu teléfono, sube una foto de tu cocina o baño y te llamamos desde tu provincia en menos de 2 minutos. Al colgar recibirás por WhatsApp el render de tu reforma + presupuesto desglosado.
|
||||
</p>
|
||||
|
||||
{/* Trust note */}
|
||||
<p className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out delay-500 text-sm text-gray-400 mt-1">
|
||||
Sin tarjeta de crédito · 14 días gratis · Cancela cuando quieras
|
||||
</p>
|
||||
|
||||
{/* Dashboard mockup */}
|
||||
<div className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out delay-600 w-full max-w-[900px] mt-8 [perspective:1200px]">
|
||||
<div
|
||||
className="bg-white border border-gray-200 rounded-2xl shadow-xl overflow-hidden transform rotate-x-4 transition-transform duration-400 ease-out hover:rotate-x-0"
|
||||
role="img"
|
||||
aria-label="Vista previa del dashboard de FlowSync"
|
||||
style={{ transform: 'rotateX(4deg)' }}
|
||||
>
|
||||
{/* Window chrome */}
|
||||
<div className="flex items-center gap-2 px-4 py-3 bg-gray-50 border-b border-gray-200">
|
||||
<span className="w-3 h-3 rounded-full shrink-0 bg-[#ff5f57]" />
|
||||
<span className="w-3 h-3 rounded-full shrink-0 bg-[#febc2e]" />
|
||||
<span className="w-3 h-3 rounded-full shrink-0 bg-[#28c840]" />
|
||||
<span className="flex-1 text-center text-xs text-gray-400 bg-white border border-gray-200 rounded-full py-[2px] px-3 max-w-[280px] mx-auto">
|
||||
app.flowsync.io/dashboard
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Mock dashboard content */}
|
||||
<div className="flex h-[380px] max-md:h-[260px]">
|
||||
{/* Sidebar */}
|
||||
<nav className="w-14 bg-gray-50 border-r border-gray-200 px-3 py-4 flex flex-col gap-3 shrink-0 max-md:hidden" aria-hidden="true">
|
||||
<div className="w-8 h-8 bg-black rounded-md mb-4" />
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`w-8 h-8 rounded-md ${
|
||||
i === 0 ? 'bg-black' : 'bg-gray-200'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 p-6 flex flex-col gap-4 overflow-hidden" aria-hidden="true">
|
||||
{/* Header row */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="h-5 w-40 bg-gray-200 rounded-sm" />
|
||||
<div className="h-8 w-24 bg-black rounded-md" />
|
||||
</div>
|
||||
|
||||
{/* Metric cards */}
|
||||
<div className="grid grid-cols-4 max-md:grid-cols-2 gap-3">
|
||||
{['#0066ff', '#00c853', '#ff9500', '#6c5ce7'].map((color, i) => (
|
||||
<div key={i} className="bg-gray-50 border border-gray-200 rounded-xl p-3 flex gap-2 items-center">
|
||||
<div
|
||||
className="w-8 h-8 rounded-md shrink-0"
|
||||
style={{ background: `${color}18`, color }}
|
||||
/>
|
||||
<div className="flex flex-col gap-[6px] flex-1">
|
||||
<div className="h-[14px] bg-gray-300 rounded-sm w-[70%]" />
|
||||
<div className="h-[10px] bg-gray-200 rounded-sm w-[90%]" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Chart area */}
|
||||
<div className="flex-1 bg-gray-50 border border-gray-200 rounded-xl p-4 overflow-hidden">
|
||||
<div className="flex items-end gap-2 h-100">
|
||||
{[60, 80, 50, 90, 70, 85, 65, 95, 75, 88].map((h, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`flex-1 rounded-t-sm animate-fadeIn opacity-100 ${
|
||||
i === 3 || i === 7 ? 'bg-black opacity-90' : 'bg-black opacity-10'
|
||||
}`}
|
||||
style={{
|
||||
height: `${h}%`,
|
||||
animationDelay: `${i * 0.05}s`,
|
||||
opacity: i === 3 || i === 7 ? 0.9 : (i % 2 === 0 ? 0.12 : 0.08)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Task list */}
|
||||
<div className="flex flex-col gap-2">
|
||||
{[85, 60, 95, 70].map((w, i) => (
|
||||
<div key={i} className="flex items-center gap-3">
|
||||
<div className="w-4 h-4 border-2 border-gray-300 rounded-sm shrink-0" />
|
||||
<div className="h-[10px] bg-gray-200 rounded-sm" style={{ width: `${w}%` }} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
{/* Stats */}
|
||||
<div className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out delay-200 flex flex-wrap gap-3">
|
||||
<button
|
||||
className="btn btn-primary btn-lg"
|
||||
onClick={() => document.querySelector('#contact')?.scrollIntoView({ behavior: 'smooth' })}
|
||||
>
|
||||
Empieza gratis
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<path d="M3 8h10M9 4l4 4-4 4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-lg"
|
||||
onClick={() => document.querySelector('#features')?.scrollIntoView({ behavior: 'smooth' })}
|
||||
>
|
||||
Ver características
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Columna derecha — formulario */}
|
||||
<div className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out delay-150 border border-gray-100 rounded-xl p-8 bg-white shadow-sm">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-black tracking-tight text-black">Recibe tu presupuesto gratis</h2>
|
||||
<p className="text-sm text-gray-400 mt-1">Sin compromiso · En menos de 5 minutos</p>
|
||||
</div>
|
||||
<LeadForm />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out delay-700 flex flex-wrap justify-center gap-12 max-md:gap-8 mt-8">
|
||||
{stats.map((stat) => (
|
||||
<div key={stat.label} className="flex flex-col items-center gap-1">
|
||||
<span className="text-3xl font-black tracking-[-0.04em] text-black">
|
||||
{stat.value}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
{stat.label}
|
||||
</span>
|
||||
<hr className="border-gray-300 mt-12 pb-8" />
|
||||
{/* Servicios */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 justify-center">
|
||||
{[
|
||||
{
|
||||
icon: (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
),
|
||||
title: 'Reformas Integrales',
|
||||
description: 'Gestionamos tu reforma de principio a fin con un objetivo claro: cumplir plazos y superar expectativas.',
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<polyline points="9 22 9 12 15 12 15 22" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
),
|
||||
title: 'Reformas de Cocinas',
|
||||
description: 'Transforma tu cocina en el espacio que siempre quisiste. Materiales de calidad, diseño a tu medida.',
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M4 12h16M4 12a2 2 0 01-2-2V6a2 2 0 012-2h16a2 2 0 012 2v4a2 2 0 01-2 2M4 12v6a2 2 0 002 2h12a2 2 0 002-2v-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
),
|
||||
title: 'Reformas de Baños',
|
||||
description: 'El baño es el espacio más personal del hogar. Te ayudamos a conseguir el resultado que mereces.',
|
||||
},
|
||||
].map(({ icon, title, description }) => (
|
||||
<div key={title} className="flex flex-col gap-4 items-center text-center">
|
||||
<div className="w-12 h-12 rounded-full bg-black flex items-center justify-center text-white">
|
||||
{icon}
|
||||
</div>
|
||||
<h3 className="text-xl font-black tracking-tight text-black">{title}</h3>
|
||||
<p className="text-gray-400 leading-relaxed text-sm max-w-[280px]">{description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
142
src/components/ReformaSlider/ReformaSlider.tsx
Normal file
142
src/components/ReformaSlider/ReformaSlider.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
|
||||
const slides = [
|
||||
{
|
||||
label: 'Cocina',
|
||||
before: 'https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=900',
|
||||
after: 'https://images.unsplash.com/photo-1556909172-54557c7e4fb7?w=900',
|
||||
},
|
||||
{
|
||||
label: 'Baño',
|
||||
before: 'https://images.unsplash.com/photo-1552321554-5fefe8c9ef14?w=900',
|
||||
after: 'https://images.unsplash.com/photo-1620626011761-996317702519?w=900',
|
||||
},
|
||||
{
|
||||
label: 'Salón',
|
||||
before: 'https://images.unsplash.com/photo-1484101403633-562f891dc89a?w=900',
|
||||
after: 'https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?w=900',
|
||||
},
|
||||
];
|
||||
|
||||
export default function ReformaSlider() {
|
||||
const [activeSlide, setActiveSlide] = useState(0);
|
||||
const [sliderX, setSliderX] = useState(50);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [visible, setVisible] = useState(true);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('opacity-100', 'translate-y-0');
|
||||
entry.target.classList.remove('opacity-0', 'translate-y-6');
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
const elements = sectionRef.current?.querySelectorAll('.reveal');
|
||||
elements?.forEach((el) => observer.observe(el));
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const handleMove = (clientX: number) => {
|
||||
if (!dragging || !containerRef.current) return;
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const x = ((clientX - rect.left) / rect.width) * 100;
|
||||
setSliderX(Math.min(Math.max(x, 5), 95));
|
||||
};
|
||||
|
||||
const switchSlide = (index: number) => {
|
||||
if (index === activeSlide) return;
|
||||
setVisible(false);
|
||||
setTimeout(() => {
|
||||
setActiveSlide(index);
|
||||
setSliderX(50);
|
||||
setVisible(true);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="bg-white py-16 md:py-24" ref={sectionRef}>
|
||||
<div className="container text-center">
|
||||
<h2 className="text-[clamp(2.5rem,5vw,4rem)] font-black tracking-[-0.04em] leading-[1.05] text-black mb-6 reveal opacity-0 translate-y-6 transition-all duration-700 ease-out">
|
||||
Ve cómo quedará tu reforma
|
||||
</h2>
|
||||
<div className="flex flex-col gap-4 reveal opacity-0 translate-y-6 transition-all duration-700 ease-out delay-100">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative w-full aspect-[16/9] rounded-xl overflow-hidden cursor-col-resize select-none"
|
||||
onMouseDown={() => setDragging(true)}
|
||||
onMouseUp={() => setDragging(false)}
|
||||
onMouseLeave={() => setDragging(false)}
|
||||
onMouseMove={(e) => handleMove(e.clientX)}
|
||||
onTouchStart={() => setDragging(true)}
|
||||
onTouchEnd={() => setDragging(false)}
|
||||
onTouchMove={(e) => handleMove(e.touches[0].clientX)}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 transition-opacity duration-300"
|
||||
style={{ opacity: visible ? 1 : 0 }}
|
||||
>
|
||||
<img
|
||||
src={slides[activeSlide].after}
|
||||
alt="Después de la reforma"
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 overflow-hidden"
|
||||
style={{ width: `${sliderX}%` }}
|
||||
>
|
||||
<img
|
||||
src={slides[activeSlide].before}
|
||||
alt="Antes de la reforma"
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
style={{ width: `${(100 / sliderX) * 100}%`, maxWidth: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
<span className="absolute top-3 left-4 bg-black/70 text-white text-xs font-medium px-3 py-1 rounded-full">
|
||||
Antes
|
||||
</span>
|
||||
<span className="absolute top-3 right-4 bg-white/90 text-black text-xs font-medium px-3 py-1 rounded-full">
|
||||
Después
|
||||
</span>
|
||||
<div
|
||||
className="absolute top-0 bottom-0 w-[2px] bg-white shadow-lg"
|
||||
style={{ left: `${sliderX}%` }}
|
||||
>
|
||||
<div className="absolute top-1/2 -translate-x-1/2 -translate-y-1/2 w-9 h-9 bg-white rounded-full shadow-xl flex items-center justify-center">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M8 4l-4 8 4 8M16 4l4 8-4 8" stroke="#1A1A1A" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 justify-center mt-2">
|
||||
{slides.map((slide, i) => (
|
||||
<button
|
||||
key={slide.label}
|
||||
onClick={() => switchSlide(i)}
|
||||
className={`relative rounded-lg overflow-hidden w-24 h-16 shrink-0 transition-all duration-200 ${i === activeSlide ? 'ring-2 ring-black ring-offset-2' : 'opacity-50 hover:opacity-80'
|
||||
}`}
|
||||
aria-label={`Ver reforma de ${slide.label}`}
|
||||
>
|
||||
<img src={slide.before} alt={slide.label} className="w-full h-full object-cover" />
|
||||
<span className="absolute bottom-1 left-1/2 -translate-x-1/2 bg-black/70 text-white text-[10px] px-2 py-0.5 rounded-full whitespace-nowrap">
|
||||
{slide.label}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user