Segundo vistaso

This commit is contained in:
unknown
2026-05-26 23:08:21 -04:00
parent bd93fb3bf2
commit 3d063113d1
16 changed files with 1158 additions and 2087 deletions

View File

@@ -1,7 +1,6 @@
'use client';
import { useState, useRef, useEffect, FormEvent } from 'react';
import styles from './ContactForm.module.css';
type FormData = {
name: string;
@@ -53,12 +52,15 @@ export default function ContactForm() {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) entry.target.classList.add(styles.visible);
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(`.${styles.reveal}`);
const elements = sectionRef.current?.querySelectorAll('.reveal');
elements?.forEach((el) => observer.observe(el));
return () => observer.disconnect();
}, []);
@@ -115,94 +117,121 @@ export default function ContactForm() {
return (
<section
className={`section ${styles.contactSection}`}
className="section bg-white"
id="contact"
ref={sectionRef}
aria-labelledby="contact-heading"
>
<div className="container">
<div className={styles.layout}>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 lg:gap-16 items-start">
{/* Left info panel */}
<div className={`${styles.reveal} ${styles.infoPanel}`}>
<span className="badge badge-dark">Contacto</span>
<h2 id="contact-heading" className={styles.title}>
<div className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out flex flex-col gap-6 lg:sticky lg:top-[104px]">
<div className="mb-2">
<span className="badge badge-dark">Contacto</span>
</div>
<h2
id="contact-heading"
className="text-[clamp(1.875rem,5vw,3rem)] font-black tracking-[-0.04em] leading-[1.05] text-black"
>
Hablemos de
<br />
tu negocio
</h2>
<p className={styles.subtitle}>
<p className="text-lg text-gray-600 leading-relaxed">
Cuéntanos qué necesitas. Nuestro equipo responderá en menos de 24 horas
con una propuesta personalizada.
</p>
{/* Contact details */}
<div className={styles.contactDetails}>
<div className={styles.contactItem}>
<div className={styles.contactIcon}>
<div className="flex flex-col gap-4 p-6 bg-gray-50 border border-gray-200 rounded-xl">
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-white border border-gray-200 rounded-lg flex items-center justify-center shrink-0 text-black">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" stroke="currentColor" strokeWidth="2" />
<path
d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"
stroke="currentColor"
strokeWidth="2"
/>
<polyline points="22,6 12,13 2,6" stroke="currentColor" strokeWidth="2" />
</svg>
</div>
<div>
<div className={styles.contactLabel}>Email</div>
<div className={styles.contactValue}>hola@flowsync.io</div>
<div className="text-xs font-semibold uppercase tracking-widest text-gray-400">Email</div>
<div className="text-base font-semibold text-black">hola@flowsync.io</div>
</div>
</div>
<div className={styles.contactItem}>
<div className={styles.contactIcon}>
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-white border border-gray-200 rounded-lg flex items-center justify-center shrink-0 text-black">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 9.62a19.79 19.79 0 01-3.07-8.63A2 2 0 012.18 0h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L6.91 7.91a16 16 0 006.18 6.18l1.28-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 16.92z" stroke="currentColor" strokeWidth="2" />
<path
d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 9.62a19.79 19.79 0 01-3.07-8.63A2 2 0 012.18 0h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L6.91 7.91a16 16 0 006.18 6.18l1.28-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 16.92z"
stroke="currentColor"
strokeWidth="2"
/>
</svg>
</div>
<div>
<div className={styles.contactLabel}>Teléfono</div>
<div className={styles.contactValue}>+1 (800) 123-4567</div>
<div className="text-xs font-semibold uppercase tracking-widest text-gray-400">Teléfono</div>
<div className="text-base font-semibold text-black">+1 (800) 123-4567</div>
</div>
</div>
<div className={styles.contactItem}>
<div className={styles.contactIcon}>
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-white border border-gray-200 rounded-lg flex items-center justify-center shrink-0 text-black">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" />
<polyline points="12 6 12 12 16 14" stroke="currentColor" strokeWidth="2" />
</svg>
</div>
<div>
<div className={styles.contactLabel}>Respuesta</div>
<div className={styles.contactValue}>Menos de 24 horas</div>
<div className="text-xs font-semibold uppercase tracking-widest text-gray-400">Respuesta</div>
<div className="text-base font-semibold text-black">Menos de 24 horas</div>
</div>
</div>
</div>
{/* Testimonial */}
<blockquote className={styles.testimonial}>
<p className={styles.testimonialText}>
<blockquote className="bg-black text-white rounded-xl p-6 flex flex-col gap-4">
<p className="text-base leading-relaxed text-white/85 italic">
&ldquo;FlowSync transformó la forma en que colaboramos. Lo que antes
tomaba días, ahora lo hacemos en horas.&rdquo;
</p>
<footer className={styles.testimonialAuthor}>
<div className={styles.authorAvatar}>MR</div>
<footer className="flex items-center gap-3">
<div className="w-10 h-10 bg-white/15 rounded-full flex items-center justify-center text-xs font-bold tracking-wider shrink-0">
MR
</div>
<div>
<div className={styles.authorName}>María Rodríguez</div>
<div className={styles.authorRole}>CTO en TechLatam</div>
<div className="text-sm font-bold">María Rodríguez</div>
<div className="text-xs text-white/50">CTO en TechLatam</div>
</div>
</footer>
</blockquote>
</div>
{/* Form panel */}
<div className={`${styles.reveal} ${styles.formPanel}`} style={{ transitionDelay: '0.15s' }}>
<div className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out delay-150 bg-white border border-gray-200 rounded-2xl p-10 max-sm:p-6 shadow-lg">
{status === 'success' ? (
<div className={styles.successState} role="alert" aria-live="polite">
<div className={styles.successIcon}>
<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">
<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" />
<path
d="M6 16l7 7L26 9"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
<h3 className={styles.successTitle}>¡Mensaje enviado!</h3>
<p className={styles.successDesc}>
<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
@@ -215,28 +244,36 @@ export default function ContactForm() {
</div>
) : (
<form
className={styles.form}
className="flex flex-col gap-5"
onSubmit={handleSubmit}
noValidate
aria-label="Formulario de contacto"
id="contact-form"
>
<div className={styles.formHeader}>
<h3 className={styles.formTitle}>Envíanos un mensaje</h3>
<p className={styles.formSubtitle}>Todos los campos marcados con * son requeridos</p>
<div className="mb-2">
<h3 className="text-2xl font-extrabold tracking-tight text-black">
Envíanos un mensaje
</h3>
<p className="text-sm text-gray-400 mt-1">
Todos los campos marcados con * son requeridos
</p>
</div>
{/* Row: Name + Email */}
<div className={styles.row}>
<div className={styles.fieldGroup}>
<label htmlFor="contact-name" className={styles.label}>
Nombre completo <span className={styles.required}>*</span>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="flex flex-col gap-2">
<label htmlFor="contact-name" className="text-sm font-semibold text-dark">
Nombre completo <span className="text-error">*</span>
</label>
<input
id="contact-name"
name="name"
type="text"
className={`${styles.input} ${errors.name && touched.name ? styles.inputError : ''}`}
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}
@@ -247,21 +284,25 @@ export default function ContactForm() {
aria-invalid={!!(errors.name && touched.name)}
/>
{errors.name && touched.name && (
<span id="name-error" className={styles.errorMsg} role="alert">
<span id="name-error" className="text-xs text-error font-medium flex items-center gap-1" role="alert">
{errors.name}
</span>
)}
</div>
<div className={styles.fieldGroup}>
<label htmlFor="contact-email" className={styles.label}>
Email <span className={styles.required}>*</span>
<div className="flex flex-col gap-2">
<label htmlFor="contact-email" className="text-sm font-semibold text-dark">
Email <span className="text-error">*</span>
</label>
<input
id="contact-email"
name="email"
type="email"
className={`${styles.input} ${errors.email && touched.email ? styles.inputError : ''}`}
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}
@@ -272,7 +313,7 @@ export default function ContactForm() {
aria-invalid={!!(errors.email && touched.email)}
/>
{errors.email && touched.email && (
<span id="email-error" className={styles.errorMsg} role="alert">
<span id="email-error" className="text-xs text-error font-medium flex items-center gap-1" role="alert">
{errors.email}
</span>
)}
@@ -280,16 +321,20 @@ export default function ContactForm() {
</div>
{/* Row: Company + Phone */}
<div className={styles.row}>
<div className={styles.fieldGroup}>
<label htmlFor="contact-company" className={styles.label}>
Empresa <span className={styles.required}>*</span>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="flex flex-col gap-2">
<label htmlFor="contact-company" className="text-sm font-semibold text-dark">
Empresa <span className="text-error">*</span>
</label>
<input
id="contact-company"
name="company"
type="text"
className={`${styles.input} ${errors.company && touched.company ? styles.inputError : ''}`}
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}
@@ -300,22 +345,26 @@ export default function ContactForm() {
aria-invalid={!!(errors.company && touched.company)}
/>
{errors.company && touched.company && (
<span id="company-error" className={styles.errorMsg} role="alert">
<span id="company-error" className="text-xs text-error font-medium flex items-center gap-1" role="alert">
{errors.company}
</span>
)}
</div>
<div className={styles.fieldGroup}>
<label htmlFor="contact-phone" className={styles.label}>
<div className="flex flex-col gap-2">
<label htmlFor="contact-phone" className="text-sm font-semibold text-dark">
Teléfono
<span className={styles.optional}> (opcional)</span>
<span className="font-normal text-gray-400"> (opcional)</span>
</label>
<input
id="contact-phone"
name="phone"
type="tel"
className={`${styles.input} ${errors.phone && touched.phone ? styles.inputError : ''}`}
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}
@@ -325,7 +374,7 @@ export default function ContactForm() {
aria-invalid={!!(errors.phone && touched.phone)}
/>
{errors.phone && touched.phone && (
<span id="phone-error" className={styles.errorMsg} role="alert">
<span id="phone-error" className="text-xs text-error font-medium flex items-center gap-1" role="alert">
{errors.phone}
</span>
)}
@@ -333,14 +382,18 @@ export default function ContactForm() {
</div>
{/* Message */}
<div className={styles.fieldGroup}>
<label htmlFor="contact-message" className={styles.label}>
Mensaje <span className={styles.required}>*</span>
<div className="flex flex-col gap-2">
<label htmlFor="contact-message" className="text-sm font-semibold text-dark">
Mensaje <span className="text-error">*</span>
</label>
<textarea
id="contact-message"
name="message"
className={`${styles.textarea} ${errors.message && touched.message ? styles.inputError : ''}`}
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}
@@ -350,15 +403,15 @@ export default function ContactForm() {
aria-describedby={errors.message && touched.message ? 'message-error' : undefined}
aria-invalid={!!(errors.message && touched.message)}
/>
<div className={styles.textareaFooter}>
<div className="flex justify-between items-center">
{errors.message && touched.message ? (
<span id="message-error" className={styles.errorMsg} role="alert">
<span id="message-error" className="text-xs text-error font-medium flex items-center gap-1" role="alert">
{errors.message}
</span>
) : (
<span />
)}
<span className={styles.charCount}>
<span className="text-xs text-gray-400 ml-auto">
{formData.message.length} caracteres
</span>
</div>
@@ -367,30 +420,41 @@ export default function ContactForm() {
{/* Submit */}
<button
type="submit"
className={`btn btn-primary btn-lg ${styles.submitBtn}`}
className="btn btn-primary btn-lg w-full justify-center mt-2 disabled:opacity-70 disabled:cursor-not-allowed disabled:transform-none"
disabled={status === 'loading'}
id="contact-submit-btn"
aria-busy={status === 'loading'}
>
{status === 'loading' ? (
<>
<span className={styles.spinner} aria-hidden="true" />
<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" />
<path
d="M2 8h12M10 4l4 4-4 4"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</>
)}
</button>
<p className={styles.privacyNote}>
<p className="text-xs text-gray-400 text-center leading-relaxed">
Al enviar, aceptas nuestra{' '}
<a href="#" className={styles.privacyLink}>política de privacidad</a>.
Nunca compartiremos tu información.
<a href="#" className="text-gray-600 underline underline-offset-2 transition-colors duration-150 hover:text-black">
política de privacidad
</a>
. Nunca compartiremos tu información.
</p>
</form>
)}