mirror of
https://github.com/marcogll/AnchorOS.git
synced 2026-03-15 19:24:32 +00:00
feat: Add Formbricks integration, update forms with webhooks, enhance navigation
- Integrate @formbricks/js for future surveys (FormbricksProvider) - Add WebhookForm component for unified form submission (contact/franchise/membership) - Update contact form with reason dropdown field - Update franchise form with new fields: estado, ciudad, socios checkbox - Update franchise benefits: manuals, training platform, RH system, investment $100k - Add Contacto link to desktop/mobile nav and footer - Update membership form to use WebhookForm with membership_id select - Update hero buttons to use #3E352E color consistently - Refactor contact/franchise pages to use new hero layout and components - Add webhook utility (lib/webhook.ts) for parallel submission to test+prod - Add email receipt hooks to booking endpoints - Update globals.css with new color variables and navigation styles - Docker configuration for deployment
This commit is contained in:
@@ -1,66 +1,244 @@
|
||||
/** @description Static services page component displaying available salon services and categories. */
|
||||
import { AnimatedLogo } from '@/components/animated-logo'
|
||||
import { RollingPhrases } from '@/components/rolling-phrases'
|
||||
|
||||
/** @description Services page with home page style structure */
|
||||
'use client'
|
||||
|
||||
import { AnimatedLogo } from '@/components/animated-logo'
|
||||
import { RollingPhrases } from '@/components/rolling-phrases'
|
||||
|
||||
/** @description Services page with home page style structure */
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
interface Service {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
duration_minutes: number
|
||||
base_price: number
|
||||
category: string
|
||||
requires_dual_artist: boolean
|
||||
is_active: boolean
|
||||
}
|
||||
|
||||
export default function ServiciosPage() {
|
||||
const services = [
|
||||
{
|
||||
category: 'Spa de Alta Gama',
|
||||
description: 'Sauna y spa excepcionales, diseñados para el rejuvenecimiento y el equilibrio.',
|
||||
items: ['Tratamientos Faciales', 'Masajes Terapéuticos', 'Hidroterapia']
|
||||
},
|
||||
{
|
||||
category: 'Arte y Manicure de Precisión',
|
||||
description: 'Estilización y técnica donde el detalle define el resultado.',
|
||||
items: ['Manicure de Precisión', 'Pedicure Spa', 'Arte en Uñas']
|
||||
},
|
||||
{
|
||||
category: 'Peinado y Maquillaje de Lujo',
|
||||
description: 'Transformaciones discretas y sofisticadas para ocasiones selectas.',
|
||||
items: ['Corte y Estilismo', 'Color Premium', 'Maquillaje Profesional']
|
||||
},
|
||||
{
|
||||
category: 'Cuidado Corporal',
|
||||
description: 'Ritual de bienestar integral.',
|
||||
items: ['Exfoliación Profunda', 'Envolturas Corporales', 'Tratamientos Reductores']
|
||||
},
|
||||
{
|
||||
category: 'Membresías Exclusivas',
|
||||
description: 'Acceso prioritario y experiencias personalizadas.',
|
||||
items: ['Gold Tier', 'Black Tier', 'VIP Tier']
|
||||
const [services, setServices] = useState<Service[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
fetchServices()
|
||||
}, [])
|
||||
|
||||
const fetchServices = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/services')
|
||||
const data = await response.json()
|
||||
if (data.success) {
|
||||
setServices(data.services.filter((s: Service) => s.is_active))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching services:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('es-MX', {
|
||||
style: 'currency',
|
||||
currency: 'MXN'
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
const formatDuration = (minutes: number) => {
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const mins = minutes % 60
|
||||
if (hours > 0) {
|
||||
return mins > 0 ? `${hours}h ${mins}min` : `${hours}h`
|
||||
}
|
||||
return `${mins} min`
|
||||
}
|
||||
|
||||
const getCategoryTitle = (category: string) => {
|
||||
const titles: Record<string, string> = {
|
||||
core: 'CORE EXPERIENCES - El corazón de Anchor 23',
|
||||
nails: 'NAIL COUTURE - Técnica invisible. Resultado impecable.',
|
||||
hair: 'HAIR FINISHING RITUALS',
|
||||
lashes: 'LASH & BROW RITUALS - Mirada definida con sutileza.',
|
||||
brows: 'LASH & BROW RITUALS - Mirada definida con sutileza.',
|
||||
events: 'EVENT EXPERIENCES - Agenda especial',
|
||||
permanent: 'PERMANENT RITUALS - Agenda limitada · Especialista certificada'
|
||||
}
|
||||
return titles[category] || category
|
||||
}
|
||||
|
||||
const getCategoryDescription = (category: string) => {
|
||||
const descriptions: Record<string, string> = {
|
||||
core: 'Rituales conscientes donde el tiempo se desacelera. Cada experiencia está diseñada para mujeres que valoran el silencio, la atención absoluta y los resultados impecables.',
|
||||
nails: 'En Anchor 23 no eliges técnicas. Cada decisión se toma internamente para lograr un resultado elegante, duradero y natural. No ofrecemos servicios de mantenimiento ni correcciones.',
|
||||
hair: 'Disponibles únicamente para clientas con experiencia Anchor el mismo día.',
|
||||
lashes: '',
|
||||
brows: '',
|
||||
events: 'Agenda especial para ocasiones selectas.',
|
||||
permanent: ''
|
||||
}
|
||||
return descriptions[category] || ''
|
||||
}
|
||||
|
||||
const groupedServices = services.reduce((acc, service) => {
|
||||
if (!acc[service.category]) {
|
||||
acc[service.category] = []
|
||||
}
|
||||
acc[service.category].push(service)
|
||||
return acc
|
||||
}, {} as Record<string, Service[]>)
|
||||
|
||||
const categoryOrder = ['core', 'nails', 'hair', 'lashes', 'brows', 'events', 'permanent']
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="section">
|
||||
<div className="section-header">
|
||||
<h1 className="section-title">Nuestros Servicios</h1>
|
||||
<p className="section-subtitle">Cargando servicios...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="section">
|
||||
<div className="section-header">
|
||||
<h1 className="section-title">Nuestros Servicios</h1>
|
||||
<p className="section-subtitle">
|
||||
Experiencias diseñadas con precisión y elegancia para clientes que valoran la exclusividad.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
{services.map((service, index) => (
|
||||
<article key={index} className="p-8 bg-white rounded-2xl shadow-lg hover:shadow-xl transition-shadow border border-gray-100">
|
||||
<h2 className="text-2xl font-semibold text-gray-900 mb-3">{service.category}</h2>
|
||||
<p className="text-gray-600 mb-4">{service.description}</p>
|
||||
<ul className="space-y-2">
|
||||
{service.items.map((item, idx) => (
|
||||
<li key={idx} className="flex items-center text-gray-700">
|
||||
<span className="w-1.5 h-1.5 bg-gray-900 rounded-full mr-2" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
<>
|
||||
<section className="hero">
|
||||
<div className="hero-content">
|
||||
<AnimatedLogo />
|
||||
<h1>Servicios</h1>
|
||||
<h2>Anchor:23</h2>
|
||||
<RollingPhrases />
|
||||
<div className="hero-actions">
|
||||
<a href="/booking/servicios" className="btn-primary">
|
||||
Reservar Cita
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 text-center">
|
||||
<a href="https://booking.anchor23.mx" className="btn-primary">
|
||||
Reservar Cita
|
||||
</a>
|
||||
<div className="hero-image">
|
||||
<div className="w-full h-96 flex items-center justify-center bg-gradient-to-br from-amber-50 to-gray-50">
|
||||
<span className="text-gray-500 text-lg">Imagen Servicios</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="foundation">
|
||||
<article>
|
||||
<h3>Experiencias</h3>
|
||||
<h4>Criterio antes que cantidad</h4>
|
||||
<p>
|
||||
Anchor 23 es un espacio privado donde el tiempo se desacelera. Aquí, cada experiencia está diseñada para mujeres que valoran el silencio, la atención absoluta y los resultados impecables.
|
||||
</p>
|
||||
<p>
|
||||
No trabajamos con volumen. Trabajamos con intención.
|
||||
</p>
|
||||
</article>
|
||||
<aside className="foundation-image">
|
||||
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-amber-50 to-gray-50">
|
||||
<span className="text-gray-500 text-lg">Imagen Experiencias</span>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section className="services-preview">
|
||||
<h3>Nuestros Servicios</h3>
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
{categoryOrder.map(category => {
|
||||
const categoryServices = groupedServices[category]
|
||||
if (!categoryServices || categoryServices.length === 0) return null
|
||||
|
||||
return (
|
||||
<div key={category} className="service-cards mb-24">
|
||||
<div className="mb-8">
|
||||
<h4 className="text-3xl font-bold text-gray-900 mb-4">
|
||||
{getCategoryTitle(category)}
|
||||
</h4>
|
||||
{getCategoryDescription(category) && (
|
||||
<p className="text-gray-600 text-lg leading-relaxed">
|
||||
{getCategoryDescription(category)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{categoryServices.map((service) => (
|
||||
<article
|
||||
key={service.id}
|
||||
className="service-card"
|
||||
>
|
||||
<div className="mb-4">
|
||||
<h5 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
{service.name}
|
||||
</h5>
|
||||
{service.description && (
|
||||
<p className="text-gray-600 text-sm leading-relaxed">
|
||||
{service.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-gray-500 text-sm">
|
||||
⏳ {formatDuration(service.duration_minutes)}
|
||||
</span>
|
||||
{service.requires_dual_artist && (
|
||||
<span className="text-xs bg-gray-100 px-2 py-1 rounded-full">Dual Artist</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-2xl font-bold text-gray-900">
|
||||
{formatCurrency(service.base_price)}
|
||||
</span>
|
||||
<a href="/booking/servicios" className="btn-primary">
|
||||
Reservar
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
<section className="testimonials">
|
||||
<h3>Lo que Define Anchor 23</h3>
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<div className="grid md:grid-cols-2 gap-6 text-left">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-red-500 text-xl">•</span>
|
||||
<span className="text-gray-700">No ofrecemos retoques ni servicios aislados</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-red-500 text-xl">•</span>
|
||||
<span className="text-gray-700">No trabajamos con prisas</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-red-500 text-xl">•</span>
|
||||
<span className="text-gray-700">No explicamos de más</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-red-500 text-xl">•</span>
|
||||
<span className="text-gray-700">No negociamos estándares</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-red-500 text-xl">•</span>
|
||||
<span className="text-gray-700">Cada experiencia está pensada para durar, sentirse y recordarse</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user