mirror of
https://github.com/marcogll/AnchorOS.git
synced 2026-03-15 13:24:27 +00:00
Fix Supabase connection issues with lazy initialization and enhanced logging
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# Dockerfile optimizado para Next.js production
|
||||
FROM node:18-alpine AS base
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
# Instalar dependencias para build
|
||||
FROM base AS deps
|
||||
|
||||
@@ -6,11 +6,24 @@ import { supabase } from '@/lib/supabase/client'
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
console.log('=== LOCATIONS API START ===')
|
||||
console.log('Locations API called with URL:', request.url)
|
||||
|
||||
// Check Supabase connection
|
||||
console.log('Supabase URL:', process.env.NEXT_PUBLIC_SUPABASE_URL)
|
||||
console.log('Supabase key exists:', !!process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY)
|
||||
// Test basic fetch to Supabase URL
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
|
||||
console.log('Testing basic connectivity to Supabase...')
|
||||
try {
|
||||
const testResponse = await fetch(`${supabaseUrl}/rest/v1/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'apikey': process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
console.log('Basic Supabase connectivity test:', testResponse.status, testResponse.statusText)
|
||||
} catch (fetchError) {
|
||||
console.error('Basic fetch test failed:', fetchError)
|
||||
}
|
||||
|
||||
console.log('Executing locations query...')
|
||||
const { data: locationsData, error: queryError } = await supabase
|
||||
@@ -19,7 +32,7 @@ export async function GET(request: NextRequest) {
|
||||
.eq('is_active', true)
|
||||
.order('name', { ascending: true })
|
||||
|
||||
console.log('Query result - data:', !!locationsData, 'error:', !!queryError)
|
||||
console.log('Query result - data exists:', !!locationsData, 'error exists:', !!queryError)
|
||||
|
||||
if (queryError) {
|
||||
console.error('Locations GET error details:', {
|
||||
@@ -32,25 +45,31 @@ export async function GET(request: NextRequest) {
|
||||
{
|
||||
error: queryError.message,
|
||||
code: queryError.code,
|
||||
details: queryError.details
|
||||
details: queryError.details,
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
console.log('Locations found:', locationsData?.length || 0)
|
||||
console.log('=== LOCATIONS API END ===')
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
locations: locationsData || [],
|
||||
count: locationsData?.length || 0
|
||||
count: locationsData?.length || 0,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('=== LOCATIONS API ERROR ===')
|
||||
console.error('Locations GET unexpected error:', error)
|
||||
console.error('Error stack:', error instanceof Error ? error.stack : 'Unknown error')
|
||||
console.error('Error type:', error instanceof Error ? error.constructor.name : typeof error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Internal server error',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
details: error instanceof Error ? error.message : 'Unknown error',
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
|
||||
@@ -6,15 +6,28 @@ import { supabase } from '@/lib/supabase/client'
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
console.log('=== SERVICES API START ===')
|
||||
console.log('Services API called with URL:', request.url)
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const locationId = searchParams.get('location_id')
|
||||
console.log('Location ID filter:', locationId)
|
||||
|
||||
// Check Supabase connection
|
||||
console.log('Supabase URL:', process.env.NEXT_PUBLIC_SUPABASE_URL)
|
||||
console.log('Supabase key exists:', !!process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY)
|
||||
// Test basic fetch to Supabase URL
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
|
||||
console.log('Testing basic connectivity to Supabase...')
|
||||
try {
|
||||
const testResponse = await fetch(`${supabaseUrl}/rest/v1/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'apikey': process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
console.log('Basic Supabase connectivity test:', testResponse.status, testResponse.statusText)
|
||||
} catch (fetchError) {
|
||||
console.error('Basic fetch test failed:', fetchError)
|
||||
}
|
||||
|
||||
let query = supabase
|
||||
.from('services')
|
||||
@@ -26,10 +39,10 @@ export async function GET(request: NextRequest) {
|
||||
query = query.eq('location_id', locationId)
|
||||
}
|
||||
|
||||
console.log('Executing query...')
|
||||
console.log('Executing Supabase query...')
|
||||
const { data: servicesData, error: queryError } = await query
|
||||
|
||||
console.log('Query result - data:', !!servicesData, 'error:', !!queryError)
|
||||
console.log('Query result - data exists:', !!servicesData, 'error exists:', !!queryError)
|
||||
|
||||
if (queryError) {
|
||||
console.error('Services GET error details:', {
|
||||
@@ -42,25 +55,31 @@ export async function GET(request: NextRequest) {
|
||||
{
|
||||
error: queryError.message,
|
||||
code: queryError.code,
|
||||
details: queryError.details
|
||||
details: queryError.details,
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
console.log('Services found:', servicesData?.length || 0)
|
||||
console.log('=== SERVICES API END ===')
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
services: servicesData || [],
|
||||
count: servicesData?.length || 0
|
||||
count: servicesData?.length || 0,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('=== SERVICES API ERROR ===')
|
||||
console.error('Services GET unexpected error:', error)
|
||||
console.error('Error stack:', error instanceof Error ? error.stack : 'Unknown error')
|
||||
console.error('Error type:', error instanceof Error ? error.constructor.name : typeof error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Internal server error',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
details: error instanceof Error ? error.message : 'Unknown error',
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState, useEffect } from 'react'
|
||||
import { AnimatedLogo } from '@/components/animated-logo'
|
||||
import { RollingPhrases } from '@/components/rolling-phrases'
|
||||
|
||||
/** @description Services page with home page style structure */
|
||||
/** @description Premium services page with elegant layout and sophisticated design */
|
||||
|
||||
interface Service {
|
||||
id: string
|
||||
@@ -57,22 +57,35 @@ export default function ServiciosPage() {
|
||||
|
||||
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.',
|
||||
core: 'CORE EXPERIENCES',
|
||||
nails: 'NAIL COUTURE',
|
||||
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'
|
||||
lashes: 'LASH & BROW RITUALS',
|
||||
brows: 'LASH & BROW RITUALS',
|
||||
events: 'EVENT EXPERIENCES',
|
||||
permanent: 'PERMANENT RITUALS'
|
||||
}
|
||||
return titles[category] || category
|
||||
}
|
||||
|
||||
const getCategorySubtitle = (category: string) => {
|
||||
const subtitles: Record<string, string> = {
|
||||
core: 'El corazón de Anchor 23',
|
||||
nails: 'Técnica invisible. Resultado impecable.',
|
||||
hair: 'Disponibles únicamente para clientas con experiencia Anchor el mismo día',
|
||||
lashes: 'Mirada definida con sutileza',
|
||||
brows: 'Mirada definida con sutileza',
|
||||
events: 'Agenda especial',
|
||||
permanent: 'Agenda limitada · Especialista certificada'
|
||||
}
|
||||
return subtitles[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.',
|
||||
hair: '',
|
||||
lashes: '',
|
||||
brows: '',
|
||||
events: 'Agenda especial para ocasiones selectas.',
|
||||
@@ -93,10 +106,10 @@ export default function ServiciosPage() {
|
||||
|
||||
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 className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-charcoal-brown mb-4"></div>
|
||||
<p className="text-xl text-charcoal-brown opacity-70">Cargando servicios...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -104,94 +117,152 @@ export default function ServiciosPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="hero">
|
||||
<div className="hero-content">
|
||||
{/* Hero Section - Simplified and Elegant */}
|
||||
<section className="relative min-h-[60vh] flex items-center justify-center pt-32 pb-20 overflow-hidden">
|
||||
{/* Background Pattern */}
|
||||
<div className="absolute inset-0 opacity-30">
|
||||
<div className="absolute inset-0" style={{
|
||||
backgroundImage: `radial-gradient(circle at 2px 2px, rgba(111, 94, 79, 0.15) 1px, transparent 0)`,
|
||||
backgroundSize: '40px 40px'
|
||||
}}></div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 max-w-5xl mx-auto px-8 text-center">
|
||||
<div className="mb-8">
|
||||
<AnimatedLogo />
|
||||
<h1>Servicios</h1>
|
||||
<h2>Anchor:23</h2>
|
||||
</div>
|
||||
<h1 className="text-6xl md:text-8xl font-bold mb-6 tracking-tight" style={{ fontFamily: 'Playfair Display, serif', color: 'var(--charcoal-brown)' }}>
|
||||
Nuestros Servicios
|
||||
</h1>
|
||||
<div className="mb-10">
|
||||
<RollingPhrases />
|
||||
<div className="hero-actions">
|
||||
<a href="/booking/servicios" className="btn-primary">
|
||||
Reservar Cita
|
||||
</div>
|
||||
<p className="text-xl md:text-2xl mb-12 max-w-3xl mx-auto leading-relaxed opacity-80" style={{ color: 'var(--charcoal-brown)' }}>
|
||||
Experiencias diseñadas para mujeres que valoran el silencio, la atención absoluta y los resultados impecables.
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-6">
|
||||
<a href="/booking/servicios" className="btn-primary text-base px-10 py-4">
|
||||
Reservar Experiencia
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<section className="foundation">
|
||||
<article>
|
||||
<h3>Experiencias</h3>
|
||||
<h4>Criterio antes que cantidad</h4>
|
||||
<p>
|
||||
{/* Philosophy Section */}
|
||||
<section className="py-24 relative" style={{ background: 'var(--soft-cream)' }}>
|
||||
<div className="max-w-6xl mx-auto px-8">
|
||||
<div className="grid md:grid-cols-2 gap-16 items-center">
|
||||
<div>
|
||||
<p className="text-sm font-semibold tracking-widest uppercase mb-4 opacity-60" style={{ color: 'var(--deep-earth)' }}>
|
||||
Nuestra Filosofía
|
||||
</p>
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-6 leading-tight" style={{ fontFamily: 'Playfair Display, serif', color: 'var(--charcoal-brown)' }}>
|
||||
Criterio antes que cantidad
|
||||
</h2>
|
||||
<p className="text-lg leading-relaxed mb-6 opacity-85" style={{ color: 'var(--charcoal-brown)' }}>
|
||||
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>
|
||||
<p className="text-lg leading-relaxed font-medium" style={{ color: 'var(--deep-earth)' }}>
|
||||
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>
|
||||
<div className="relative h-96 rounded-2xl overflow-hidden shadow-2xl">
|
||||
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-amber-100 via-stone-100 to-neutral-100">
|
||||
<span className="text-neutral-400 text-lg font-light">Imagen Experiencias</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="services-preview">
|
||||
<h3>Nuestros Servicios</h3>
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
{/* Services Catalog */}
|
||||
<section className="py-32" style={{ background: 'var(--bone-white)' }}>
|
||||
<div className="max-w-7xl mx-auto px-8">
|
||||
{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">
|
||||
<div key={category} className="mb-32 last:mb-0">
|
||||
{/* Category Header */}
|
||||
<div className="mb-16 text-center max-w-4xl mx-auto">
|
||||
<p className="text-sm font-semibold tracking-widest uppercase mb-3 opacity-60" style={{ color: 'var(--deep-earth)' }}>
|
||||
{getCategorySubtitle(category)}
|
||||
</p>
|
||||
<h3 className="text-4xl md:text-5xl font-bold mb-6 tracking-tight" style={{ fontFamily: 'Playfair Display, serif', color: 'var(--charcoal-brown)' }}>
|
||||
{getCategoryTitle(category)}
|
||||
</h4>
|
||||
</h3>
|
||||
{getCategoryDescription(category) && (
|
||||
<p className="text-gray-600 text-lg leading-relaxed">
|
||||
<p className="text-lg leading-relaxed opacity-75" style={{ color: 'var(--charcoal-brown)' }}>
|
||||
{getCategoryDescription(category)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{/* Service Cards Grid */}
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{categoryServices.map((service) => (
|
||||
<article
|
||||
key={service.id}
|
||||
className="service-card"
|
||||
className="group relative rounded-2xl p-8 transition-all duration-500 hover:shadow-2xl hover:-translate-y-2"
|
||||
style={{
|
||||
background: 'var(--soft-cream)',
|
||||
border: '1px solid transparent'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--mocha-taupe)'
|
||||
e.currentTarget.style.background = 'var(--bone-white)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = 'transparent'
|
||||
e.currentTarget.style.background = 'var(--soft-cream)'
|
||||
}}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<h5 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
{/* Service Header */}
|
||||
<div className="mb-6">
|
||||
<h4 className="text-2xl font-bold mb-3 leading-tight group-hover:opacity-90 transition-opacity" style={{ fontFamily: 'Playfair Display, serif', color: 'var(--charcoal-brown)' }}>
|
||||
{service.name}
|
||||
</h5>
|
||||
</h4>
|
||||
{service.description && (
|
||||
<p className="text-gray-600 text-sm leading-relaxed">
|
||||
<p className="text-base leading-relaxed opacity-75" style={{ color: 'var(--charcoal-brown)' }}>
|
||||
{service.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-gray-500 text-sm">
|
||||
⏳ {formatDuration(service.duration_minutes)}
|
||||
{/* Service Meta */}
|
||||
<div className="flex items-center gap-4 mb-6 pb-6 border-b" style={{ borderColor: 'var(--mocha-taupe)' }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-5 h-5 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24" style={{ color: 'var(--deep-earth)' }}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span className="text-sm font-medium opacity-70" style={{ color: 'var(--charcoal-brown)' }}>
|
||||
{formatDuration(service.duration_minutes)}
|
||||
</span>
|
||||
</div>
|
||||
{service.requires_dual_artist && (
|
||||
<span className="text-xs bg-gray-100 px-2 py-1 rounded-full">Dual Artist</span>
|
||||
<span className="text-xs font-semibold px-3 py-1 rounded-full" style={{ background: 'var(--mocha-taupe)', color: 'var(--bone-white)' }}>
|
||||
Dual Artist
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Price and CTA */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-2xl font-bold text-gray-900">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-wider mb-1 opacity-50" style={{ color: 'var(--charcoal-brown)' }}>Desde</p>
|
||||
<p className="text-3xl font-bold" style={{ fontFamily: 'Playfair Display, serif', color: 'var(--charcoal-brown)' }}>
|
||||
{formatCurrency(service.base_price)}
|
||||
</span>
|
||||
<a href="/booking/servicios" className="btn-primary">
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="/booking/servicios"
|
||||
className="inline-flex items-center justify-center px-6 py-3 text-sm font-medium rounded-lg transition-all duration-300 hover:shadow-lg hover:-translate-y-1"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, var(--deep-earth), var(--charcoal-brown))',
|
||||
color: 'var(--bone-white)'
|
||||
}}
|
||||
>
|
||||
Reservar
|
||||
</a>
|
||||
</div>
|
||||
@@ -201,38 +272,55 @@ export default function ServiciosPage() {
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
{/* Values Section */}
|
||||
<section className="py-24 relative" style={{ background: 'var(--soft-cream)' }}>
|
||||
<div className="max-w-5xl mx-auto px-8">
|
||||
<h3 className="text-4xl md:text-5xl font-bold mb-16 text-center" style={{ fontFamily: 'Playfair Display, serif', color: 'var(--charcoal-brown)' }}>
|
||||
Lo que Define Anchor 23
|
||||
</h3>
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
<div className="space-y-6">
|
||||
{[
|
||||
'No ofrecemos retoques ni servicios aislados',
|
||||
'No trabajamos con prisas',
|
||||
'No explicamos de más'
|
||||
].map((text, idx) => (
|
||||
<div key={idx} className="flex items-start gap-4 p-6 rounded-xl transition-all duration-300 hover:shadow-lg" style={{ background: 'var(--bone-white)' }}>
|
||||
<div className="flex-shrink-0 w-2 h-2 rounded-full mt-2" style={{ background: 'var(--brick-red)' }}></div>
|
||||
<p className="text-lg leading-relaxed" style={{ color: 'var(--charcoal-brown)' }}>{text}</p>
|
||||
</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 className="space-y-6">
|
||||
{[
|
||||
'No negociamos estándares',
|
||||
'Cada experiencia está pensada para durar, sentirse y recordarse'
|
||||
].map((text, idx) => (
|
||||
<div key={idx} className="flex items-start gap-4 p-6 rounded-xl transition-all duration-300 hover:shadow-lg" style={{ background: 'var(--bone-white)' }}>
|
||||
<div className="flex-shrink-0 w-2 h-2 rounded-full mt-2" style={{ background: 'var(--brick-red)' }}></div>
|
||||
<p className="text-lg leading-relaxed" style={{ color: 'var(--charcoal-brown)' }}>{text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Final CTA */}
|
||||
<section className="py-24 text-center" style={{ background: 'var(--bone-white)' }}>
|
||||
<div className="max-w-3xl mx-auto px-8">
|
||||
<h3 className="text-4xl md:text-5xl font-bold mb-6" style={{ fontFamily: 'Playfair Display, serif', color: 'var(--charcoal-brown)' }}>
|
||||
¿Lista para tu experiencia?
|
||||
</h3>
|
||||
<p className="text-xl mb-10 opacity-75" style={{ color: 'var(--charcoal-brown)' }}>
|
||||
Reserva tu cita y descubre lo que significa una atención verdaderamente personalizada.
|
||||
</p>
|
||||
<a href="/booking/servicios" className="btn-primary text-base px-12 py-4 inline-block">
|
||||
Reservar Ahora
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
// Lazy initialization to ensure env vars are available at runtime
|
||||
let supabaseInstance: ReturnType<typeof createClient> | null = null
|
||||
|
||||
// Public Supabase client for client-side operations
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
||||
function getSupabaseClient() {
|
||||
if (!supabaseInstance) {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
|
||||
|
||||
console.log('=== SUPABASE CLIENT INIT ===')
|
||||
console.log('SUPABASE_URL available:', !!supabaseUrl)
|
||||
console.log('SUPABASE_ANON_KEY available:', !!supabaseAnonKey)
|
||||
console.log('SUPABASE_URL value:', supabaseUrl)
|
||||
console.log('SUPABASE_ANON_KEY preview:', supabaseAnonKey ? supabaseAnonKey.substring(0, 20) + '...' : 'null')
|
||||
|
||||
if (!supabaseUrl || !supabaseAnonKey) {
|
||||
throw new Error(`Missing Supabase environment variables: URL=${!!supabaseUrl}, KEY=${!!supabaseAnonKey}`)
|
||||
}
|
||||
|
||||
supabaseInstance = createClient(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
autoRefreshToken: true,
|
||||
persistSession: true,
|
||||
@@ -12,4 +26,18 @@ export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
||||
}
|
||||
})
|
||||
|
||||
console.log('Supabase client initialized successfully')
|
||||
}
|
||||
|
||||
return supabaseInstance
|
||||
}
|
||||
|
||||
// Public Supabase client for client-side operations
|
||||
export const supabase = new Proxy({} as ReturnType<typeof createClient>, {
|
||||
get(target, prop) {
|
||||
const client = getSupabaseClient()
|
||||
return client[prop as keyof typeof client]
|
||||
}
|
||||
})
|
||||
|
||||
export default supabase
|
||||
|
||||
Reference in New Issue
Block a user