Site restructure + admin panel for self-management
- Split page.tsx into server component + home-client.tsx (data-driven) - Add /about page with extended biography, Art as Ritual, Art in Motion - Add /admin panel (password-protected) for artworks, events, services CRUD - Restructure nav: Home | Art | Services | Re Evolution Art | About | Contact - Services now show "How it works" / "Who it is for" bullet points - Gallery: featured artworks (large), grid, sold/private carousel, lightbox with metadata - "Future Gatherings" renamed to "PULSAR" - JSON data layer with auto-seed from hardcoded content (/data volume) - Image upload API with file type validation (max 10MB) - HMAC session auth (Web Crypto API, Edge-compatible) - Docker: named volume xhivart-data, /data directory, ADMIN_PASSWORD env Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b377c9a3b1
commit
630312c7ea
|
|
@ -27,6 +27,9 @@ RUN adduser --system --uid 1001 nextjs
|
|||
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
# Create data directory for JSON storage and uploads
|
||||
RUN mkdir -p /data && chown nextjs:nodejs /data
|
||||
|
||||
# Set the correct permission for prerender cache
|
||||
RUN mkdir .next
|
||||
RUN chown nextjs:nodejs .next
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ services:
|
|||
- SMTP_USER=${SMTP_USER}
|
||||
- SMTP_PASS=${SMTP_PASS}
|
||||
- SMTP_FROM=${SMTP_FROM}
|
||||
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
||||
volumes:
|
||||
- xhivart-data:/data
|
||||
networks:
|
||||
- traefik-public
|
||||
labels:
|
||||
|
|
@ -24,6 +27,9 @@ services:
|
|||
retries: 3
|
||||
start_period: 15s
|
||||
|
||||
volumes:
|
||||
xhivart-data:
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
external: true
|
||||
|
|
|
|||
|
|
@ -12,6 +12,19 @@ const nextConfig: NextConfig = {
|
|||
pathname: "/images/**",
|
||||
},
|
||||
],
|
||||
remotePatterns: [
|
||||
{
|
||||
hostname: "localhost",
|
||||
},
|
||||
],
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: "/uploads/:path*",
|
||||
destination: "/api/uploads/:path*",
|
||||
},
|
||||
];
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,257 @@
|
|||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
export const metadata = {
|
||||
title: 'About Ximena Xaguar | XHIVA ART',
|
||||
description: 'Multidisciplinary visionary artist working at the intersection of art, ritual and embodied presence.',
|
||||
};
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<>
|
||||
{/* Navigation — simplified for sub-page */}
|
||||
<nav className="fixed top-0 left-0 right-0 z-50 glass">
|
||||
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
|
||||
<Link href="/" className="text-2xl font-light tracking-widest">
|
||||
XHIVA ART
|
||||
</Link>
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<Link href="/" className="nav-link">Home</Link>
|
||||
<Link href="/#art" className="nav-link">Art</Link>
|
||||
<Link href="/#services" className="nav-link">Services</Link>
|
||||
<Link href="/#reevolution" className="nav-link">Re Evolution Art</Link>
|
||||
<Link href="/about" className="nav-link" style={{ color: 'var(--accent-gold)' }}>About</Link>
|
||||
<Link href="/#contact" className="nav-link">Contact</Link>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
{/* Hero */}
|
||||
<section className="pt-32 pb-16 px-6">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="grid md:grid-cols-2 gap-12 items-center">
|
||||
{/* Portrait */}
|
||||
<div className="relative aspect-[3/4] rounded-2xl overflow-hidden shadow-xl">
|
||||
<Image
|
||||
src="/images/about/portrait-1.jpg"
|
||||
alt="Ximena Xaguar"
|
||||
fill
|
||||
className="object-cover object-[30%_center]"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
THE ARTIST
|
||||
</p>
|
||||
<h1 className="text-4xl md:text-5xl font-light mb-6">
|
||||
About Ximena
|
||||
</h1>
|
||||
<div className="divider" style={{ margin: '1.5rem 0' }}></div>
|
||||
<p className="text-lg leading-relaxed mb-6 opacity-80">
|
||||
Ximena Xaguar is a multidisciplinary visionary artist, healer and cultural
|
||||
producer based in Zürich, Switzerland, with deep roots in Bolivia. Her
|
||||
work weaves ancestral memory with contemporary expression across painting,
|
||||
ceremony, immersive gatherings and community art.
|
||||
</p>
|
||||
<p className="text-lg leading-relaxed mb-6 opacity-80">
|
||||
With over fifteen years of practice guiding Temazcal ceremonies, crystal
|
||||
healing sessions and transformative group experiences, Ximena creates spaces
|
||||
where art becomes a lived experience — a bridge between inner worlds
|
||||
and shared reality.
|
||||
</p>
|
||||
<p className="text-lg leading-relaxed opacity-80">
|
||||
Her paintings emerge from cycles of transformation, shadow work, intuitive
|
||||
vision and ancestral cosmovision. Each artwork is a portal — an ally
|
||||
for contemplation, energetic coherence and spiritual insight.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Extended Biography */}
|
||||
<section className="section bg-white">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
BIOGRAPHY
|
||||
</p>
|
||||
<h2 className="text-3xl md:text-4xl font-light mb-6">
|
||||
The Path
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 text-lg leading-relaxed opacity-80">
|
||||
<p>
|
||||
Born in La Paz, Bolivia, Ximena grew up immersed in the rich cultural
|
||||
tapestry of the Andes — a world where art, ceremony and daily life
|
||||
are inseparable. This early foundation shaped her understanding of art as
|
||||
something alive, relational and deeply connected to land and community.
|
||||
</p>
|
||||
<p>
|
||||
She studied Fine Arts and later expanded her practice through years of
|
||||
apprenticeship in ancestral healing traditions across Bolivia and Peru.
|
||||
The Temazcal (sweatlodge) became a central pillar of her ceremonial work,
|
||||
which she has guided for over fifteen years.
|
||||
</p>
|
||||
<p>
|
||||
Moving to Switzerland, Ximena founded Re Evolution Art — a cultural
|
||||
platform bridging South American ancestral wisdom with European contemporary
|
||||
art. Through events like Visionary Art Week Zürich, TRIBAL Nights and
|
||||
PULSAR, she creates spaces where artists, musicians, ritualists and seekers
|
||||
converge.
|
||||
</p>
|
||||
<p>
|
||||
Her artistic practice spans large-scale canvas painting, murals, live
|
||||
performance and collaborative installation. Each work draws on symbolic
|
||||
language, universal cosmovision and the transformative power of colour
|
||||
and form.
|
||||
</p>
|
||||
<p>
|
||||
Today, Ximena continues to paint, guide ceremonies and produce cultural
|
||||
events from her base in Zürich, while maintaining deep connections
|
||||
to her Bolivian roots and the broader network of visionary artists and
|
||||
healers worldwide.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Second Portrait */}
|
||||
<section className="section">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="grid md:grid-cols-2 gap-12 items-center">
|
||||
<div>
|
||||
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
PRACTICE
|
||||
</p>
|
||||
<h2 className="text-3xl md:text-4xl font-light mb-6">
|
||||
Art as Ritual
|
||||
</h2>
|
||||
<div className="divider" style={{ margin: '1.5rem 0' }}></div>
|
||||
<p className="text-lg leading-relaxed mb-6 opacity-80">
|
||||
For Ximena, every painting is a ceremony. The studio becomes a ritual
|
||||
space — candles, incense, music and intention set the container
|
||||
for creation. The painting process mirrors the inner journey: death
|
||||
and rebirth, shadow and light, dissolution and integration.
|
||||
</p>
|
||||
<p className="text-lg leading-relaxed opacity-80">
|
||||
Her works are not decorative objects but living presences — portals
|
||||
that continue to work on the viewer long after the first encounter.
|
||||
Collectors and participants consistently describe a felt sense of
|
||||
connection, activation and deep recognition when engaging with her art.
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative aspect-[3/4] rounded-2xl overflow-hidden shadow-xl">
|
||||
<Image
|
||||
src="/images/about/portrait-artist-1.webp"
|
||||
alt="Ximena painting"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Art in Motion */}
|
||||
<section className="section bg-white">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
THE PROCESS
|
||||
</p>
|
||||
<h2 className="text-3xl md:text-4xl font-light mb-6">
|
||||
Art in Motion
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{[
|
||||
{ src: '/images/about/painting-process.webp', alt: 'Ximena painting', position: 'top' },
|
||||
{ src: '/images/art/mural-bio-centro.webp', alt: 'Mural Bio Centro Guembe' },
|
||||
{ src: '/images/about/portrait-2.jpg', alt: 'Ximena painting mural' },
|
||||
{ src: '/images/about/portrait-3.jpg', alt: 'Ximena in studio', position: 'top' },
|
||||
{ src: '/images/about/portrait-4.jpg', alt: 'Ximena at ceremony' },
|
||||
{ src: '/images/about/pachamama.jpg', alt: 'Ritual with smoke' },
|
||||
].map((photo, index) => (
|
||||
<div key={index} className="relative aspect-[4/3] rounded-lg overflow-hidden group">
|
||||
<Image
|
||||
src={photo.src}
|
||||
alt={photo.alt}
|
||||
fill
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
style={photo.position ? { objectPosition: photo.position } : undefined}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<section className="section text-center">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<h2 className="text-3xl font-light mb-6">Explore Further</h2>
|
||||
<div className="divider"></div>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center mt-8">
|
||||
<Link href="/#art" className="btn-outline">
|
||||
View Gallery
|
||||
</Link>
|
||||
<Link href="/#services" className="btn-outline">
|
||||
Services
|
||||
</Link>
|
||||
<Link href="/#contact" className="btn-filled">
|
||||
Get in Touch
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="dark-section py-16 px-6">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="grid md:grid-cols-4 gap-12 mb-12">
|
||||
<div className="md:col-span-2">
|
||||
<h3 className="text-2xl font-light tracking-widest mb-4">XHIVA ART</h3>
|
||||
<p className="text-sm opacity-70 leading-relaxed mb-6">
|
||||
Visionary Art · Ceremony · Cultural Experiences
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-sans-alt text-xs tracking-widest mb-4">EXPLORE</h4>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Link href="/#art" className="footer-link">Art</Link>
|
||||
<Link href="/#services" className="footer-link">Services</Link>
|
||||
<Link href="/#reevolution" className="footer-link">Re Evolution Art</Link>
|
||||
<Link href="/about" className="footer-link">About</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-sans-alt text-xs tracking-widest mb-4">CONNECT</h4>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Link href="/#contact" className="footer-link">Contact</Link>
|
||||
<a href="https://instagram.com/ximena_xaguar" target="_blank" rel="noopener noreferrer" className="footer-link">
|
||||
@ximena_xaguar
|
||||
</a>
|
||||
<a href="https://www.facebook.com/XimenaXhivart" target="_blank" rel="noopener noreferrer" className="footer-link">
|
||||
Facebook
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-white/10 pt-8 text-center">
|
||||
<p className="font-sans-alt text-xs tracking-widest opacity-50">
|
||||
© {new Date().getFullYear()} XHIVA ART. ALL RIGHTS RESERVED.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { ArtEvent } from '@/lib/types';
|
||||
|
||||
export default function AdminEvents() {
|
||||
const [events, setEvents] = useState<ArtEvent[]>([]);
|
||||
const [editing, setEditing] = useState<ArtEvent | null>(null);
|
||||
const [isNew, setIsNew] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fetchEvents = async () => {
|
||||
const res = await fetch('/api/admin/events');
|
||||
setEvents(await res.json());
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => { fetchEvents(); }, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!editing) return;
|
||||
const method = isNew ? 'POST' : 'PUT';
|
||||
const url = isNew ? '/api/admin/events' : `/api/admin/events/${editing.id}`;
|
||||
await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(editing),
|
||||
});
|
||||
setEditing(null);
|
||||
setIsNew(false);
|
||||
fetchEvents();
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('Delete this event?')) return;
|
||||
await fetch(`/api/admin/events/${id}`, { method: 'DELETE' });
|
||||
fetchEvents();
|
||||
};
|
||||
|
||||
const handleUpload = async (file: File): Promise<string> => {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
const res = await fetch('/api/admin/upload', { method: 'POST', body: form });
|
||||
const data = await res.json();
|
||||
return data.url || '';
|
||||
};
|
||||
|
||||
const newEvent = (): ArtEvent => ({
|
||||
id: `event-${Date.now()}`,
|
||||
title: '',
|
||||
description: '',
|
||||
image: '',
|
||||
date: '',
|
||||
link: '',
|
||||
section: 'reevolution',
|
||||
sortOrder: events.length + 1,
|
||||
});
|
||||
|
||||
if (loading) return <div className="p-8 text-gray-400">Loading...</div>;
|
||||
|
||||
const reevolutionEvents = events.filter(e => e.section === 'reevolution').sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
const xhivaEvents = events.filter(e => e.section === 'xhiva').sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-2xl font-light">Events</h1>
|
||||
<button
|
||||
onClick={() => { setEditing(newEvent()); setIsNew(true); }}
|
||||
className="px-4 py-2 bg-[#2d2d2d] text-white rounded-lg text-sm hover:bg-[#c9a962] transition-colors"
|
||||
>
|
||||
+ Add Event
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Edit Modal */}
|
||||
{editing && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl max-w-lg w-full max-h-[90vh] overflow-y-auto p-6">
|
||||
<h2 className="text-xl font-light mb-4">{isNew ? 'New Event' : 'Edit Event'}</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-1">TITLE</label>
|
||||
<input value={editing.title} onChange={(e) => setEditing({...editing, title: e.target.value})}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962]" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-1">DESCRIPTION</label>
|
||||
<textarea value={editing.description} onChange={(e) => setEditing({...editing, description: e.target.value})} rows={3}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962] resize-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-1">IMAGE</label>
|
||||
{editing.image && (
|
||||
<div className="w-20 h-14 bg-gray-100 rounded overflow-hidden mb-2">
|
||||
<img src={editing.image} alt="" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
)}
|
||||
<label className="text-xs text-blue-600 hover:text-blue-800 cursor-pointer block mb-2">
|
||||
Upload image
|
||||
<input type="file" accept="image/*" className="hidden" onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) { const url = await handleUpload(file); if (url) setEditing({...editing, image: url}); }
|
||||
}} />
|
||||
</label>
|
||||
<input value={editing.image} onChange={(e) => setEditing({...editing, image: e.target.value})}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962]"
|
||||
placeholder="Or paste image path" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-1">DATE</label>
|
||||
<input value={editing.date} onChange={(e) => setEditing({...editing, date: e.target.value})}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962]"
|
||||
placeholder="e.g. March 2026" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-1">SECTION</label>
|
||||
<select value={editing.section} onChange={(e) => setEditing({...editing, section: e.target.value as 'xhiva' | 'reevolution'})}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962]">
|
||||
<option value="reevolution">Re Evolution Art</option>
|
||||
<option value="xhiva">XHIVA</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-1">LINK</label>
|
||||
<input value={editing.link} onChange={(e) => setEditing({...editing, link: e.target.value})}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962]"
|
||||
placeholder="https://..." />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-1">SORT ORDER</label>
|
||||
<input type="number" value={editing.sortOrder} onChange={(e) => setEditing({...editing, sortOrder: parseInt(e.target.value) || 0})}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 mt-6">
|
||||
<button onClick={handleSave} className="px-6 py-2 bg-[#2d2d2d] text-white rounded-lg text-sm hover:bg-[#c9a962] transition-colors">
|
||||
Save
|
||||
</button>
|
||||
<button onClick={() => { setEditing(null); setIsNew(false); }} className="px-6 py-2 border rounded-lg text-sm hover:bg-gray-50">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Event List by section */}
|
||||
{reevolutionEvents.length > 0 && (
|
||||
<div className="mb-8">
|
||||
<h2 className="text-sm tracking-widest text-gray-500 mb-3">RE EVOLUTION ART</h2>
|
||||
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
|
||||
<EventTable events={reevolutionEvents} onEdit={(e) => { setEditing(e); setIsNew(false); }} onDelete={handleDelete} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{xhivaEvents.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-sm tracking-widest text-gray-500 mb-3">XHIVA</h2>
|
||||
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
|
||||
<EventTable events={xhivaEvents} onEdit={(e) => { setEditing(e); setIsNew(false); }} onDelete={handleDelete} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EventTable({ events, onEdit, onDelete }: { events: ArtEvent[]; onEdit: (e: ArtEvent) => void; onDelete: (id: string) => void }) {
|
||||
return (
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-gray-50 text-left">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-xs tracking-widest text-gray-500">IMAGE</th>
|
||||
<th className="px-4 py-3 text-xs tracking-widest text-gray-500">TITLE</th>
|
||||
<th className="px-4 py-3 text-xs tracking-widest text-gray-500">DATE</th>
|
||||
<th className="px-4 py-3 text-xs tracking-widest text-gray-500">ACTIONS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{events.map((event) => (
|
||||
<tr key={event.id} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-3">
|
||||
{event.image && (
|
||||
<div className="w-16 h-10 rounded overflow-hidden bg-gray-100">
|
||||
<img src={event.image} alt="" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 font-medium">{event.title}</td>
|
||||
<td className="px-4 py-3 text-gray-500">{event.date || '—'}</td>
|
||||
<td className="px-4 py-3">
|
||||
<button onClick={() => onEdit(event)} className="text-blue-600 hover:text-blue-800 mr-3 text-xs">Edit</button>
|
||||
<button onClick={() => onDelete(event.id)} className="text-red-600 hover:text-red-800 text-xs">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
|
||||
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
if (pathname === '/admin/login') {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
{ href: '/admin', label: 'Artworks', icon: '🖼' },
|
||||
{ href: '/admin/events', label: 'Events', icon: '📅' },
|
||||
{ href: '/admin/services', label: 'Services', icon: '✨' },
|
||||
];
|
||||
|
||||
const handleLogout = async () => {
|
||||
await fetch('/api/admin/logout', { method: 'POST' });
|
||||
router.push('/admin/login');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-56 bg-[#2d2d2d] text-white flex flex-col shrink-0">
|
||||
<div className="p-5 border-b border-white/10">
|
||||
<Link href="/admin" className="text-lg font-light tracking-wider">XHIVA ART</Link>
|
||||
<p className="text-xs text-gray-400 mt-1">Admin</p>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 py-4">
|
||||
{navItems.map((item) => {
|
||||
const isActive = item.href === '/admin'
|
||||
? pathname === '/admin' || pathname === '/admin/artworks'
|
||||
: pathname.startsWith(item.href);
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`flex items-center gap-3 px-5 py-3 text-sm transition-colors ${
|
||||
isActive ? 'bg-white/10 text-[#c9a962]' : 'text-gray-300 hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
<span>{item.icon}</span>
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-white/10 space-y-2">
|
||||
<a
|
||||
href="/"
|
||||
target="_blank"
|
||||
className="block text-xs text-gray-400 hover:text-white transition-colors px-1"
|
||||
>
|
||||
View Site →
|
||||
</a>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="block text-xs text-gray-400 hover:text-red-400 transition-colors px-1"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 overflow-auto">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function AdminLogin() {
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/admin/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
setError('Invalid password');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
router.push('/admin');
|
||||
} catch {
|
||||
setError('Something went wrong');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-[#faf8f5] px-4">
|
||||
<div className="w-full max-w-sm">
|
||||
<h1 className="text-3xl font-light tracking-wider text-center mb-8">XHIVA ART</h1>
|
||||
<p className="text-center text-sm text-gray-500 mb-8 font-[var(--font-montserrat)]">Admin Panel</p>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-2 font-[var(--font-montserrat)]" htmlFor="password">
|
||||
PASSWORD
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-4 py-3 border border-gray-200 rounded-lg focus:outline-none focus:border-[#c9a962] transition-colors"
|
||||
placeholder="Enter admin password"
|
||||
autoFocus
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-red-600 text-sm">{error}</p>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full py-3 bg-[#2d2d2d] text-[#faf8f5] rounded-full text-xs tracking-widest uppercase hover:bg-[#c9a962] transition-colors disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Signing in...' : 'Sign In'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="text-center mt-8">
|
||||
<a href="/" className="text-xs text-gray-400 hover:text-[#c9a962] tracking-widest uppercase">
|
||||
Back to site
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { Artwork } from '@/lib/types';
|
||||
|
||||
function ImageUpload({ currentImage, onUpload }: { currentImage: string; onUpload: (url: string) => void }) {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
setUploading(true);
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
try {
|
||||
const res = await fetch('/api/admin/upload', { method: 'POST', body: form });
|
||||
const data = await res.json();
|
||||
if (data.url) onUpload(data.url);
|
||||
} catch { /* ignore */ }
|
||||
setUploading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{currentImage && (
|
||||
<div className="w-20 h-20 bg-gray-100 rounded overflow-hidden mb-2">
|
||||
<img src={currentImage} alt="" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
)}
|
||||
<label className="text-xs text-blue-600 hover:text-blue-800 cursor-pointer">
|
||||
{uploading ? 'Uploading...' : currentImage ? 'Change image' : 'Upload image'}
|
||||
<input type="file" accept="image/*" onChange={handleUpload} className="hidden" disabled={uploading} />
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AdminArtworks() {
|
||||
const [artworks, setArtworks] = useState<Artwork[]>([]);
|
||||
const [editing, setEditing] = useState<Artwork | null>(null);
|
||||
const [isNew, setIsNew] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fetchArtworks = async () => {
|
||||
const res = await fetch('/api/admin/artworks');
|
||||
setArtworks(await res.json());
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => { fetchArtworks(); }, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!editing) return;
|
||||
const method = isNew ? 'POST' : 'PUT';
|
||||
const url = isNew ? '/api/admin/artworks' : `/api/admin/artworks/${editing.id}`;
|
||||
await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(editing),
|
||||
});
|
||||
setEditing(null);
|
||||
setIsNew(false);
|
||||
fetchArtworks();
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('Delete this artwork?')) return;
|
||||
await fetch(`/api/admin/artworks/${id}`, { method: 'DELETE' });
|
||||
fetchArtworks();
|
||||
};
|
||||
|
||||
const newArtwork = (): Artwork => ({
|
||||
id: `art-${Date.now()}`,
|
||||
title: '',
|
||||
medium: '',
|
||||
dimensions: '',
|
||||
year: '',
|
||||
price: '',
|
||||
image: '',
|
||||
category: 'available',
|
||||
featured: false,
|
||||
sortOrder: artworks.length + 1,
|
||||
});
|
||||
|
||||
if (loading) return <div className="p-8 text-gray-400">Loading...</div>;
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-2xl font-light">Artworks</h1>
|
||||
<button
|
||||
onClick={() => { setEditing(newArtwork()); setIsNew(true); }}
|
||||
className="px-4 py-2 bg-[#2d2d2d] text-white rounded-lg text-sm hover:bg-[#c9a962] transition-colors"
|
||||
>
|
||||
+ Add Artwork
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Edit Modal */}
|
||||
{editing && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl max-w-lg w-full max-h-[90vh] overflow-y-auto p-6">
|
||||
<h2 className="text-xl font-light mb-4">{isNew ? 'New Artwork' : 'Edit Artwork'}</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-1">TITLE</label>
|
||||
<input value={editing.title} onChange={(e) => setEditing({...editing, title: e.target.value})}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962]" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-1">MEDIUM</label>
|
||||
<input value={editing.medium} onChange={(e) => setEditing({...editing, medium: e.target.value})}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962]" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-1">DIMENSIONS</label>
|
||||
<input value={editing.dimensions} onChange={(e) => setEditing({...editing, dimensions: e.target.value})}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-1">YEAR</label>
|
||||
<input value={editing.year} onChange={(e) => setEditing({...editing, year: e.target.value})}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962]" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-1">PRICE</label>
|
||||
<input value={editing.price} onChange={(e) => setEditing({...editing, price: e.target.value})}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962]" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-1">IMAGE</label>
|
||||
<ImageUpload currentImage={editing.image} onUpload={(url) => setEditing({...editing, image: url})} />
|
||||
<input value={editing.image} onChange={(e) => setEditing({...editing, image: e.target.value})}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm mt-2 focus:outline-none focus:border-[#c9a962]"
|
||||
placeholder="Or paste image path" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-1">CATEGORY</label>
|
||||
<select value={editing.category} onChange={(e) => setEditing({...editing, category: e.target.value as Artwork['category']})}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962]">
|
||||
<option value="available">Available</option>
|
||||
<option value="sold">Sold</option>
|
||||
<option value="private">Private Collection</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-1">SORT ORDER</label>
|
||||
<input type="number" value={editing.sortOrder} onChange={(e) => setEditing({...editing, sortOrder: parseInt(e.target.value) || 0})}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962]" />
|
||||
</div>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input type="checkbox" checked={editing.featured} onChange={(e) => setEditing({...editing, featured: e.target.checked})} />
|
||||
Featured artwork
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex gap-3 mt-6">
|
||||
<button onClick={handleSave} className="px-6 py-2 bg-[#2d2d2d] text-white rounded-lg text-sm hover:bg-[#c9a962] transition-colors">
|
||||
Save
|
||||
</button>
|
||||
<button onClick={() => { setEditing(null); setIsNew(false); }} className="px-6 py-2 border rounded-lg text-sm hover:bg-gray-50">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Artwork List */}
|
||||
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-gray-50 text-left">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-xs tracking-widest text-gray-500">IMAGE</th>
|
||||
<th className="px-4 py-3 text-xs tracking-widest text-gray-500">TITLE</th>
|
||||
<th className="px-4 py-3 text-xs tracking-widest text-gray-500">CATEGORY</th>
|
||||
<th className="px-4 py-3 text-xs tracking-widest text-gray-500">FEATURED</th>
|
||||
<th className="px-4 py-3 text-xs tracking-widest text-gray-500">ACTIONS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{artworks.sort((a, b) => a.sortOrder - b.sortOrder).map((artwork) => (
|
||||
<tr key={artwork.id} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-3">
|
||||
{artwork.image && (
|
||||
<div className="w-12 h-12 rounded overflow-hidden bg-gray-100">
|
||||
<img src={artwork.image} alt="" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 font-medium">{artwork.title}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`text-xs px-2 py-1 rounded-full ${
|
||||
artwork.category === 'available' ? 'bg-green-100 text-green-700' :
|
||||
artwork.category === 'sold' ? 'bg-red-100 text-red-700' :
|
||||
'bg-purple-100 text-purple-700'
|
||||
}`}>
|
||||
{artwork.category}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">{artwork.featured ? 'Yes' : ''}</td>
|
||||
<td className="px-4 py-3">
|
||||
<button onClick={() => { setEditing(artwork); setIsNew(false); }} className="text-blue-600 hover:text-blue-800 mr-3 text-xs">
|
||||
Edit
|
||||
</button>
|
||||
<button onClick={() => handleDelete(artwork.id)} className="text-red-600 hover:text-red-800 text-xs">
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { Service } from '@/lib/types';
|
||||
|
||||
export default function AdminServices() {
|
||||
const [services, setServices] = useState<Service[]>([]);
|
||||
const [editing, setEditing] = useState<Service | null>(null);
|
||||
const [isNew, setIsNew] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fetchServices = async () => {
|
||||
const res = await fetch('/api/admin/services');
|
||||
setServices(await res.json());
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => { fetchServices(); }, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!editing) return;
|
||||
const method = isNew ? 'POST' : 'PUT';
|
||||
const url = isNew ? '/api/admin/services' : `/api/admin/services/${editing.id}`;
|
||||
await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(editing),
|
||||
});
|
||||
setEditing(null);
|
||||
setIsNew(false);
|
||||
fetchServices();
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('Delete this service?')) return;
|
||||
await fetch(`/api/admin/services/${id}`, { method: 'DELETE' });
|
||||
fetchServices();
|
||||
};
|
||||
|
||||
const handleUpload = async (file: File): Promise<string> => {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
const res = await fetch('/api/admin/upload', { method: 'POST', body: form });
|
||||
const data = await res.json();
|
||||
return data.url || '';
|
||||
};
|
||||
|
||||
const updateBullet = (field: 'howItWorks' | 'whoItIsFor', index: number, value: string) => {
|
||||
if (!editing) return;
|
||||
const arr = [...editing[field]];
|
||||
arr[index] = value;
|
||||
setEditing({ ...editing, [field]: arr });
|
||||
};
|
||||
|
||||
const addBullet = (field: 'howItWorks' | 'whoItIsFor') => {
|
||||
if (!editing) return;
|
||||
setEditing({ ...editing, [field]: [...editing[field], ''] });
|
||||
};
|
||||
|
||||
const removeBullet = (field: 'howItWorks' | 'whoItIsFor', index: number) => {
|
||||
if (!editing) return;
|
||||
setEditing({ ...editing, [field]: editing[field].filter((_, i) => i !== index) });
|
||||
};
|
||||
|
||||
const newService = (): Service => ({
|
||||
id: `service-${Date.now()}`,
|
||||
title: '',
|
||||
subtitle: '',
|
||||
duration: '',
|
||||
description: '',
|
||||
howItWorks: ['', '', ''],
|
||||
whoItIsFor: ['', '', ''],
|
||||
image: '',
|
||||
color: 'lavender',
|
||||
calendlyLink: '',
|
||||
highlighted: false,
|
||||
sortOrder: services.length + 1,
|
||||
});
|
||||
|
||||
if (loading) return <div className="p-8 text-gray-400">Loading...</div>;
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-2xl font-light">Services</h1>
|
||||
<button
|
||||
onClick={() => { setEditing(newService()); setIsNew(true); }}
|
||||
className="px-4 py-2 bg-[#2d2d2d] text-white rounded-lg text-sm hover:bg-[#c9a962] transition-colors"
|
||||
>
|
||||
+ Add Service
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Edit Modal */}
|
||||
{editing && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto p-6">
|
||||
<h2 className="text-xl font-light mb-4">{isNew ? 'New Service' : 'Edit Service'}</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-1">TITLE</label>
|
||||
<input value={editing.title} onChange={(e) => setEditing({...editing, title: e.target.value})}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962]" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-1">SUBTITLE</label>
|
||||
<input value={editing.subtitle} onChange={(e) => setEditing({...editing, subtitle: e.target.value})}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-1">DURATION</label>
|
||||
<input value={editing.duration} onChange={(e) => setEditing({...editing, duration: e.target.value})}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962]" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-1">COLOR</label>
|
||||
<select value={editing.color} onChange={(e) => setEditing({...editing, color: e.target.value})}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962]">
|
||||
<option value="lavender">Lavender</option>
|
||||
<option value="pink">Pink</option>
|
||||
<option value="mint">Mint</option>
|
||||
<option value="rose">Rose</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-1">DESCRIPTION</label>
|
||||
<textarea value={editing.description} onChange={(e) => setEditing({...editing, description: e.target.value})} rows={3}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962] resize-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-1">IMAGE</label>
|
||||
{editing.image && (
|
||||
<div className="w-20 h-14 bg-gray-100 rounded overflow-hidden mb-2">
|
||||
<img src={editing.image} alt="" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
)}
|
||||
<label className="text-xs text-blue-600 hover:text-blue-800 cursor-pointer block mb-2">
|
||||
Upload image
|
||||
<input type="file" accept="image/*" className="hidden" onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) { const url = await handleUpload(file); if (url) setEditing({...editing, image: url}); }
|
||||
}} />
|
||||
</label>
|
||||
<input value={editing.image} onChange={(e) => setEditing({...editing, image: e.target.value})}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962]"
|
||||
placeholder="Or paste image path" />
|
||||
</div>
|
||||
|
||||
{/* How it works */}
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-2">HOW IT WORKS</label>
|
||||
{editing.howItWorks.map((bullet, i) => (
|
||||
<div key={i} className="flex gap-2 mb-2">
|
||||
<input value={bullet} onChange={(e) => updateBullet('howItWorks', i, e.target.value)}
|
||||
className="flex-1 px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962]" />
|
||||
<button onClick={() => removeBullet('howItWorks', i)} className="text-red-400 hover:text-red-600 text-xs px-2">x</button>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={() => addBullet('howItWorks')} className="text-xs text-blue-600 hover:text-blue-800">+ Add bullet</button>
|
||||
</div>
|
||||
|
||||
{/* Who it is for */}
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-2">WHO IT IS FOR</label>
|
||||
{editing.whoItIsFor.map((bullet, i) => (
|
||||
<div key={i} className="flex gap-2 mb-2">
|
||||
<input value={bullet} onChange={(e) => updateBullet('whoItIsFor', i, e.target.value)}
|
||||
className="flex-1 px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962]" />
|
||||
<button onClick={() => removeBullet('whoItIsFor', i)} className="text-red-400 hover:text-red-600 text-xs px-2">x</button>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={() => addBullet('whoItIsFor')} className="text-xs text-blue-600 hover:text-blue-800">+ Add bullet</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-1">CALENDLY LINK</label>
|
||||
<input value={editing.calendlyLink} onChange={(e) => setEditing({...editing, calendlyLink: e.target.value})}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962]"
|
||||
placeholder="https://calendly.com/..." />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs tracking-widest mb-1">SORT ORDER</label>
|
||||
<input type="number" value={editing.sortOrder} onChange={(e) => setEditing({...editing, sortOrder: parseInt(e.target.value) || 0})}
|
||||
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-[#c9a962]" />
|
||||
</div>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input type="checkbox" checked={editing.highlighted} onChange={(e) => setEditing({...editing, highlighted: e.target.checked})} />
|
||||
Highlighted (gold ring)
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex gap-3 mt-6">
|
||||
<button onClick={handleSave} className="px-6 py-2 bg-[#2d2d2d] text-white rounded-lg text-sm hover:bg-[#c9a962] transition-colors">
|
||||
Save
|
||||
</button>
|
||||
<button onClick={() => { setEditing(null); setIsNew(false); }} className="px-6 py-2 border rounded-lg text-sm hover:bg-gray-50">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Services List */}
|
||||
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-gray-50 text-left">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-xs tracking-widest text-gray-500">IMAGE</th>
|
||||
<th className="px-4 py-3 text-xs tracking-widest text-gray-500">TITLE</th>
|
||||
<th className="px-4 py-3 text-xs tracking-widest text-gray-500">SUBTITLE</th>
|
||||
<th className="px-4 py-3 text-xs tracking-widest text-gray-500">COLOR</th>
|
||||
<th className="px-4 py-3 text-xs tracking-widest text-gray-500">ACTIONS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{services.sort((a, b) => a.sortOrder - b.sortOrder).map((service) => (
|
||||
<tr key={service.id} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-3">
|
||||
{service.image && (
|
||||
<div className="w-16 h-10 rounded overflow-hidden bg-gray-100">
|
||||
<img src={service.image} alt="" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 font-medium">{service.title}</td>
|
||||
<td className="px-4 py-3 text-gray-500">{service.subtitle}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`inline-block w-4 h-4 rounded-full ${
|
||||
service.color === 'lavender' ? 'bg-[#d4c4e8]' :
|
||||
service.color === 'pink' ? 'bg-[#e8c4d4]' :
|
||||
service.color === 'mint' ? 'bg-[#c4e8d4]' :
|
||||
'bg-[#e8d4c4]'
|
||||
}`} />
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<button onClick={() => { setEditing(service); setIsNew(false); }} className="text-blue-600 hover:text-blue-800 mr-3 text-xs">Edit</button>
|
||||
<button onClick={() => handleDelete(service.id)} className="text-red-600 hover:text-red-800 text-xs">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getArtwork, updateArtwork, deleteArtwork } from '@/lib/data';
|
||||
|
||||
export async function GET(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const artwork = getArtwork(id);
|
||||
if (!artwork) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
return NextResponse.json(artwork);
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
try {
|
||||
const body = await request.json();
|
||||
const updated = updateArtwork(id, body);
|
||||
if (!updated) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
return NextResponse.json(updated);
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const deleted = deleteArtwork(id);
|
||||
if (!deleted) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getArtworks, addArtwork } from '@/lib/data';
|
||||
import type { Artwork } from '@/lib/types';
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json(getArtworks());
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const artwork: Artwork = {
|
||||
id: body.id || `art-${Date.now()}`,
|
||||
title: body.title || '',
|
||||
medium: body.medium || '',
|
||||
dimensions: body.dimensions || '',
|
||||
year: body.year || '',
|
||||
price: body.price || '',
|
||||
image: body.image || '',
|
||||
category: body.category || 'available',
|
||||
featured: body.featured || false,
|
||||
sortOrder: body.sortOrder ?? getArtworks().length + 1,
|
||||
};
|
||||
addArtwork(artwork);
|
||||
return NextResponse.json(artwork, { status: 201 });
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getEvent, updateEvent, deleteEvent } from '@/lib/data';
|
||||
|
||||
export async function GET(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const event = getEvent(id);
|
||||
if (!event) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
return NextResponse.json(event);
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
try {
|
||||
const body = await request.json();
|
||||
const updated = updateEvent(id, body);
|
||||
if (!updated) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
return NextResponse.json(updated);
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const deleted = deleteEvent(id);
|
||||
if (!deleted) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getEvents, addEvent } from '@/lib/data';
|
||||
import type { ArtEvent } from '@/lib/types';
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json(getEvents());
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const event: ArtEvent = {
|
||||
id: body.id || `event-${Date.now()}`,
|
||||
title: body.title || '',
|
||||
description: body.description || '',
|
||||
image: body.image || '',
|
||||
date: body.date || '',
|
||||
link: body.link || '',
|
||||
section: body.section || 'reevolution',
|
||||
objectPosition: body.objectPosition || undefined,
|
||||
sortOrder: body.sortOrder ?? getEvents().length + 1,
|
||||
};
|
||||
addEvent(event);
|
||||
return NextResponse.json(event, { status: 201 });
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { validatePassword, createSessionToken, getSessionCookie } from '@/lib/auth';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { password } = await request.json();
|
||||
|
||||
if (!password || !validatePassword(password)) {
|
||||
return NextResponse.json({ error: 'Invalid password' }, { status: 401 });
|
||||
}
|
||||
|
||||
const token = await createSessionToken();
|
||||
const response = NextResponse.json({ success: true });
|
||||
response.headers.set('Set-Cookie', getSessionCookie(token));
|
||||
return response;
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { clearSessionCookie } from '@/lib/auth';
|
||||
|
||||
export async function POST() {
|
||||
const response = NextResponse.json({ success: true });
|
||||
response.headers.set('Set-Cookie', clearSessionCookie());
|
||||
return response;
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getService, updateService, deleteService } from '@/lib/data';
|
||||
|
||||
export async function GET(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const service = getService(id);
|
||||
if (!service) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
return NextResponse.json(service);
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
try {
|
||||
const body = await request.json();
|
||||
const updated = updateService(id, body);
|
||||
if (!updated) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
return NextResponse.json(updated);
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const deleted = deleteService(id);
|
||||
if (!deleted) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServices, addService } from '@/lib/data';
|
||||
import type { Service } from '@/lib/types';
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json(getServices());
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const service: Service = {
|
||||
id: body.id || `service-${Date.now()}`,
|
||||
title: body.title || '',
|
||||
subtitle: body.subtitle || '',
|
||||
duration: body.duration || '',
|
||||
description: body.description || '',
|
||||
howItWorks: body.howItWorks || [],
|
||||
whoItIsFor: body.whoItIsFor || [],
|
||||
image: body.image || '',
|
||||
color: body.color || 'lavender',
|
||||
calendlyLink: body.calendlyLink || '',
|
||||
highlighted: body.highlighted || false,
|
||||
sortOrder: body.sortOrder ?? getServices().length + 1,
|
||||
};
|
||||
addService(service);
|
||||
return NextResponse.json(service, { status: 201 });
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || '/data';
|
||||
const UPLOAD_DIR = join(DATA_DIR, 'uploads');
|
||||
const MAX_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const file = formData.get('file') as File | null;
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: 'No file provided' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (file.size > MAX_SIZE) {
|
||||
return NextResponse.json({ error: 'File too large (max 10MB)' }, { status: 400 });
|
||||
}
|
||||
|
||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
return NextResponse.json({ error: 'Invalid file type' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!existsSync(UPLOAD_DIR)) {
|
||||
mkdirSync(UPLOAD_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
const ext = file.name.split('.').pop() || 'jpg';
|
||||
const safeName = file.name.replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
const filename = `${Date.now()}-${safeName}`;
|
||||
const filepath = join(UPLOAD_DIR, filename);
|
||||
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
writeFileSync(filepath, buffer);
|
||||
|
||||
return NextResponse.json({
|
||||
url: `/uploads/${filename}`,
|
||||
filename,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
});
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Upload failed' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { join, extname } from 'path';
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || '/data';
|
||||
const UPLOAD_DIR = join(DATA_DIR, 'uploads');
|
||||
|
||||
const MIME_TYPES: Record<string, string> = {
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.png': 'image/png',
|
||||
'.webp': 'image/webp',
|
||||
'.gif': 'image/gif',
|
||||
};
|
||||
|
||||
export async function GET(_request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
|
||||
const { path } = await params;
|
||||
const filename = path.join('/');
|
||||
|
||||
// Prevent directory traversal
|
||||
if (filename.includes('..')) {
|
||||
return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
|
||||
}
|
||||
|
||||
const filepath = join(UPLOAD_DIR, filename);
|
||||
|
||||
if (!existsSync(filepath)) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const ext = extname(filepath).toLowerCase();
|
||||
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
||||
const buffer = readFileSync(filepath);
|
||||
|
||||
return new NextResponse(buffer, {
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': 'public, max-age=31536000, immutable',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,843 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import type { Artwork, ArtEvent, Service } from '@/lib/types';
|
||||
|
||||
// Navigation Component
|
||||
function Navigation() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className="fixed top-0 left-0 right-0 z-50 glass">
|
||||
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
|
||||
<Link href="/" className="text-2xl font-light tracking-widest">
|
||||
XHIVA ART
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<Link href="/" className="nav-link">Home</Link>
|
||||
<Link href="#art" className="nav-link">Art</Link>
|
||||
<Link href="#services" className="nav-link">Services</Link>
|
||||
<Link href="#reevolution" className="nav-link">Re Evolution Art</Link>
|
||||
<Link href="/about" className="nav-link">About</Link>
|
||||
<Link href="#contact" className="nav-link">Contact</Link>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<button
|
||||
className="md:hidden p-2"
|
||||
onClick={() => setMobileMenuOpen(true)}
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<line x1="3" y1="6" x2="21" y2="6"/>
|
||||
<line x1="3" y1="12" x2="21" y2="12"/>
|
||||
<line x1="3" y1="18" x2="21" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="mobile-menu">
|
||||
<button
|
||||
className="absolute top-6 right-6 p-2"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
<Link href="/" className="nav-link text-xl" onClick={() => setMobileMenuOpen(false)}>Home</Link>
|
||||
<Link href="#art" className="nav-link text-xl" onClick={() => setMobileMenuOpen(false)}>Art</Link>
|
||||
<Link href="#services" className="nav-link text-xl" onClick={() => setMobileMenuOpen(false)}>Services</Link>
|
||||
<Link href="#reevolution" className="nav-link text-xl" onClick={() => setMobileMenuOpen(false)}>Re Evolution Art</Link>
|
||||
<Link href="/about" className="nav-link text-xl" onClick={() => setMobileMenuOpen(false)}>About</Link>
|
||||
<Link href="#contact" className="nav-link text-xl" onClick={() => setMobileMenuOpen(false)}>Contact</Link>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Hero Section
|
||||
function HeroSection() {
|
||||
return (
|
||||
<section className="min-h-screen relative flex flex-col items-center justify-center text-center px-6 pt-20">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src="/images/hero/hero-main.jpg"
|
||||
alt="Ximena Xaguar"
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-[var(--bg-cream)]/90 via-[var(--bg-cream)]/70 to-[var(--bg-cream)]/90" />
|
||||
</div>
|
||||
|
||||
<div className="max-w-4xl mx-auto relative z-10">
|
||||
<p className="font-sans-alt text-xs tracking-[0.3em] text-[var(--text-muted)] mb-6 fade-in">
|
||||
MULTIDISCIPLINARY VISIONARY ARTIST
|
||||
</p>
|
||||
<h1 className="text-5xl md:text-7xl lg:text-8xl font-light tracking-wider mb-8 fade-in stagger-1">
|
||||
XIMENA XAGUAR
|
||||
</h1>
|
||||
<div className="divider fade-in stagger-2"></div>
|
||||
<p className="text-lg md:text-xl max-w-2xl mx-auto mb-16 leading-relaxed opacity-80 fade-in stagger-2">
|
||||
Visionary art, immersive experiences and ceremonial practice.
|
||||
Creating at the intersection of art, ritual and embodied presence.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center fade-in stagger-3">
|
||||
<Link href="#art" className="btn-outline">
|
||||
Explore the Gallery
|
||||
</Link>
|
||||
<Link href="#reevolution" className="btn-filled">
|
||||
Discover Re Evolution Art
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 animate-bounce opacity-50 z-10">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M12 5v14M5 12l7 7 7-7"/>
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Art Gallery Section — enhanced with featured, metadata, sold carousel
|
||||
function GallerySection({ artworks }: { artworks: Artwork[] }) {
|
||||
const [selectedArtwork, setSelectedArtwork] = useState<Artwork | null>(null);
|
||||
|
||||
const featured = artworks.filter(a => a.featured && a.category !== 'sold').sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
const available = artworks.filter(a => !a.featured && a.category === 'available').sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
const soldOrPrivate = artworks.filter(a => a.category === 'sold' || a.category === 'private').sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
|
||||
return (
|
||||
<section id="art" className="section bg-white">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
GALLERY
|
||||
</p>
|
||||
<h2 className="text-4xl md:text-5xl font-light mb-6">
|
||||
Visionary Artworks
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
</div>
|
||||
|
||||
{/* Featured Artworks — large format */}
|
||||
{featured.length > 0 && (
|
||||
<div className="grid md:grid-cols-3 gap-6 mb-12">
|
||||
{featured.map((artwork) => (
|
||||
<div
|
||||
key={artwork.id}
|
||||
className="relative aspect-[3/4] rounded-xl overflow-hidden group cursor-pointer shadow-lg"
|
||||
onClick={() => setSelectedArtwork(artwork)}
|
||||
>
|
||||
<Image
|
||||
src={artwork.image}
|
||||
alt={artwork.title}
|
||||
fill
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4 translate-y-full group-hover:translate-y-0 transition-transform duration-300">
|
||||
<h3 className="text-white text-lg font-light">{artwork.title}</h3>
|
||||
{artwork.medium && <p className="text-white/70 text-xs mt-1">{artwork.medium}</p>}
|
||||
{artwork.price && <p className="text-[var(--accent-gold)] text-sm mt-1">{artwork.price}</p>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* All Artworks Grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{available.map((artwork) => (
|
||||
<div
|
||||
key={artwork.id}
|
||||
className="relative aspect-square rounded-lg overflow-hidden group cursor-pointer"
|
||||
onClick={() => setSelectedArtwork(artwork)}
|
||||
>
|
||||
<Image
|
||||
src={artwork.image}
|
||||
alt={artwork.title}
|
||||
fill
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-110"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-colors duration-300 flex items-end justify-center pb-4">
|
||||
<span className="text-white opacity-0 group-hover:opacity-100 transition-opacity duration-300 font-sans-alt text-xs tracking-widest">
|
||||
{artwork.title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Sold & Private Collections */}
|
||||
{soldOrPrivate.length > 0 && (
|
||||
<div className="mt-16">
|
||||
<h3 className="text-center text-2xl font-light mb-2">Sold & Private Collections</h3>
|
||||
<div className="divider"></div>
|
||||
<div className="flex gap-4 overflow-x-auto pb-4 mt-8 snap-x snap-mandatory">
|
||||
{soldOrPrivate.map((artwork) => (
|
||||
<div
|
||||
key={artwork.id}
|
||||
className="relative w-48 h-48 rounded-lg overflow-hidden group cursor-pointer shrink-0 snap-start opacity-80 hover:opacity-100 transition-opacity"
|
||||
onClick={() => setSelectedArtwork(artwork)}
|
||||
>
|
||||
<Image
|
||||
src={artwork.image}
|
||||
alt={artwork.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-black/60 p-2">
|
||||
<p className="text-white text-xs font-sans-alt tracking-wider">{artwork.title}</p>
|
||||
<p className="text-white/50 text-[10px] uppercase">{artwork.category}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Lightbox Modal */}
|
||||
{selectedArtwork && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/90 p-4 md:p-8"
|
||||
onClick={() => setSelectedArtwork(null)}
|
||||
>
|
||||
<button
|
||||
className="absolute top-4 right-4 md:top-6 md:right-6 z-50 text-white/70 hover:text-white transition-colors p-2"
|
||||
onClick={() => setSelectedArtwork(null)}
|
||||
aria-label="Close"
|
||||
>
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div className="relative w-full h-full max-w-6xl max-h-[85vh] flex flex-col items-center justify-center">
|
||||
<div
|
||||
className="relative w-full flex-1 min-h-0"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Image
|
||||
src={selectedArtwork.image}
|
||||
alt={selectedArtwork.title}
|
||||
fill
|
||||
className="object-contain"
|
||||
sizes="(max-width: 768px) 100vw, 90vw"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="mt-4 text-center"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<p className="text-white/90 font-light text-lg">{selectedArtwork.title}</p>
|
||||
<div className="flex items-center justify-center gap-4 mt-2 text-white/50 font-sans-alt text-xs tracking-wider">
|
||||
{selectedArtwork.medium && <span>{selectedArtwork.medium}</span>}
|
||||
{selectedArtwork.dimensions && <span>{selectedArtwork.dimensions}</span>}
|
||||
{selectedArtwork.year && <span>{selectedArtwork.year}</span>}
|
||||
</div>
|
||||
{selectedArtwork.price && selectedArtwork.category === 'available' && (
|
||||
<p className="text-[var(--accent-gold)] text-sm mt-2">{selectedArtwork.price}</p>
|
||||
)}
|
||||
{selectedArtwork.category === 'sold' && (
|
||||
<p className="text-red-400/70 text-xs mt-2 uppercase tracking-widest">Sold</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Services Section — restructured with bullet points
|
||||
function ServicesSection({ services }: { services: Service[] }) {
|
||||
const sorted = [...services].sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
|
||||
return (
|
||||
<section id="services" className="section">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
OFFERINGS
|
||||
</p>
|
||||
<h2 className="text-4xl md:text-5xl font-light mb-6">
|
||||
Art, Ceremony & Experience
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
<p className="text-lg max-w-2xl mx-auto opacity-80">
|
||||
Personal offerings rooted in decades of embodied practice — art, ceremony,
|
||||
group immersion and creative exploration.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
{sorted.map((service) => (
|
||||
<div key={service.id} className={`group ${service.highlighted ? 'ring-2 ring-[var(--accent-gold)] rounded-xl' : ''}`}>
|
||||
<div className="relative aspect-[4/3] rounded-t-xl overflow-hidden">
|
||||
<Image
|
||||
src={service.image}
|
||||
alt={service.title}
|
||||
fill
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
<div className={`service-card ${service.color} rounded-t-none`}>
|
||||
<h3 className="text-2xl font-light mb-2">{service.title}</h3>
|
||||
<p className="font-sans-alt text-xs tracking-widest text-[var(--text-muted)] mb-1">
|
||||
{service.subtitle}
|
||||
</p>
|
||||
<p className="font-sans-alt text-xs tracking-widest text-[var(--accent-gold)] mb-4">
|
||||
{service.duration}
|
||||
</p>
|
||||
<p className="text-sm leading-relaxed opacity-80 mb-4">
|
||||
{service.description}
|
||||
</p>
|
||||
|
||||
{/* How it works */}
|
||||
{service.howItWorks.length > 0 && service.howItWorks[0] && (
|
||||
<div className="text-left mt-4 mb-3">
|
||||
<p className="font-sans-alt text-[10px] tracking-[0.2em] text-[var(--text-muted)] mb-2 uppercase">How it works</p>
|
||||
<ul className="space-y-1">
|
||||
{service.howItWorks.filter(Boolean).map((item, i) => (
|
||||
<li key={i} className="text-sm opacity-80 flex items-start gap-2">
|
||||
<span className="text-[var(--accent-gold)] mt-0.5">•</span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Who it is for */}
|
||||
{service.whoItIsFor.length > 0 && service.whoItIsFor[0] && (
|
||||
<div className="text-left mb-4">
|
||||
<p className="font-sans-alt text-[10px] tracking-[0.2em] text-[var(--text-muted)] mb-2 uppercase">Who it is for</p>
|
||||
<ul className="space-y-1">
|
||||
{service.whoItIsFor.filter(Boolean).map((item, i) => (
|
||||
<li key={i} className="text-sm opacity-80 flex items-start gap-2">
|
||||
<span className="text-[var(--accent-gold)] mt-0.5">•</span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{service.calendlyLink && (
|
||||
<div className="mt-4">
|
||||
<a href={service.calendlyLink} target="_blank" rel="noopener noreferrer" className="btn-outline text-xs">
|
||||
Book Now
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mt-20">
|
||||
<a href="https://booking.xhiva.art" target="_blank" rel="noopener noreferrer" className="btn-outline">
|
||||
Book a Session
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Re Evolution Art Section — clearly separated
|
||||
function ReEvolutionSection({ events }: { events: ArtEvent[] }) {
|
||||
const reevolutionEvents = events
|
||||
.filter(e => e.section === 'reevolution')
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
|
||||
return (
|
||||
<section id="reevolution" className="section dark-section">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
{/* Logo */}
|
||||
<div className="relative w-full max-w-96 aspect-[3/2] mx-auto mb-2 overflow-hidden">
|
||||
<Image
|
||||
src="/images/reevolution/logo.png"
|
||||
alt="Re Evolution Art Logo"
|
||||
fill
|
||||
className="object-contain object-center scale-150"
|
||||
/>
|
||||
</div>
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
CULTURAL PLATFORM
|
||||
</p>
|
||||
<h2 style={{ fontFamily: "'Krown', sans-serif", fontWeight: 700 }} className="text-4xl md:text-5xl mb-6">
|
||||
Re Evolution Art
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-lg max-w-3xl mx-auto opacity-80">
|
||||
A visionary cultural platform based in Switzerland and rooted in Bolivia. Through exhibitions,
|
||||
immersive gatherings, TRIBAL events and collaborative projects, we bring together artists,
|
||||
ritualists, musicians and independent creators engaged in inner work and cultural dialogue.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{reevolutionEvents.map((event) => (
|
||||
<div key={event.id} className="event-card group overflow-hidden">
|
||||
<div className="relative aspect-video rounded-lg overflow-hidden mb-4">
|
||||
<Image
|
||||
src={event.image}
|
||||
alt={event.title}
|
||||
fill
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
style={event.objectPosition ? { objectPosition: event.objectPosition } : undefined}
|
||||
/>
|
||||
</div>
|
||||
<h3 style={{ fontFamily: "'Krown', sans-serif", fontWeight: 700 }} className="text-xl mb-2 text-[var(--accent-gold)]">
|
||||
{event.title}
|
||||
</h3>
|
||||
{event.date && (
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-xs text-[var(--accent-gold)]/70 mb-2">
|
||||
{event.date}
|
||||
</p>
|
||||
)}
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-sm opacity-70 leading-relaxed">
|
||||
{event.description}
|
||||
</p>
|
||||
{event.link && (
|
||||
<a href={event.link} target="_blank" rel="noopener noreferrer"
|
||||
className="inline-block mt-3 text-xs text-[var(--accent-gold)] hover:text-white transition-colors"
|
||||
style={{ fontFamily: "'Narrenschiff', sans-serif" }}>
|
||||
Learn more →
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Social Links */}
|
||||
<div className="flex justify-center gap-6 mt-16">
|
||||
<a
|
||||
href="https://www.instagram.com/reevolutionart"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-outline btn-outline-light"
|
||||
style={{ fontFamily: "'Narrenschiff', sans-serif" }}
|
||||
>
|
||||
Instagram
|
||||
</a>
|
||||
<a
|
||||
href="https://www.facebook.com/groups/383432167715274/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-outline btn-outline-light"
|
||||
style={{ fontFamily: "'Narrenschiff', sans-serif" }}
|
||||
>
|
||||
Facebook Community
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Testimonials Section
|
||||
function TestimonialsSection() {
|
||||
const testimonials = [
|
||||
{
|
||||
quote: "The crystal healing session created an immediate connection and brought my soul forth. I deeply appreciate the support through the process.",
|
||||
author: "Dieter Katterbach",
|
||||
},
|
||||
{
|
||||
quote: "The sweat lodge ceremony was powerfully moving, highlighting the integration of healing, compassion and authentic expression.",
|
||||
author: "Kermit Goodman",
|
||||
},
|
||||
{
|
||||
quote: "The experience was powerfully transformative \u2014 it changed my understanding of ceremony itself.",
|
||||
author: "Verana Bailowitz",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="section bg-white">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
TESTIMONIALS
|
||||
</p>
|
||||
<h2 className="text-4xl md:text-5xl font-light mb-6">
|
||||
Words of Gratitude
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8">
|
||||
{testimonials.map((testimonial, index) => (
|
||||
<div key={index} className="testimonial-card">
|
||||
<span className="quote-mark">“</span>
|
||||
<p className="testimonial mb-6">
|
||||
{testimonial.quote}
|
||||
</p>
|
||||
<p className="font-sans-alt text-xs tracking-widest text-[var(--accent-gold)]">
|
||||
— {testimonial.author}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Work With Me Section
|
||||
function WorkWithMeSection() {
|
||||
return (
|
||||
<section className="section relative overflow-hidden">
|
||||
<div className="absolute inset-0 z-0 bg-[#0f0f0f]">
|
||||
<Image
|
||||
src="/images/art/wayra-bg.webp"
|
||||
alt="Wayra"
|
||||
fill
|
||||
className="object-contain opacity-40"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="max-w-3xl mx-auto relative z-10">
|
||||
<div className="text-center">
|
||||
<h2 className="text-4xl md:text-5xl font-light mb-6 text-[var(--text-light)]">
|
||||
Work With Me
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
<p className="text-lg leading-relaxed mb-16 opacity-80 max-w-2xl mx-auto text-[var(--text-light)]">
|
||||
This work is for those ready to meet themselves honestly with courage,
|
||||
sensitivity and presence.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||
<a href="https://booking.xhiva.art" target="_blank" rel="noopener noreferrer" className="btn-outline btn-outline-light">
|
||||
Book a Session
|
||||
</a>
|
||||
<Link href="#contact" className="btn-filled" style={{ background: 'var(--accent-gold)', borderColor: 'var(--accent-gold)' }}>
|
||||
Contact Me
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Contact Section
|
||||
function ContactSection() {
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!name.trim() || !email.trim() || !message.trim()) {
|
||||
setStatus('error');
|
||||
setErrorMessage('Please fill in all fields.');
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus('loading');
|
||||
setErrorMessage('');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: name.trim(), email: email.trim(), message: message.trim() }),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || 'Something went wrong');
|
||||
}
|
||||
|
||||
setStatus('success');
|
||||
setName('');
|
||||
setEmail('');
|
||||
setMessage('');
|
||||
} catch (err) {
|
||||
setStatus('error');
|
||||
setErrorMessage(err instanceof Error ? err.message : 'Failed to send message. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="contact" className="section bg-white">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="text-center">
|
||||
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
GET IN TOUCH
|
||||
</p>
|
||||
<h2 className="text-4xl md:text-5xl font-light mb-6">
|
||||
Get in Touch
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
<p className="text-lg leading-relaxed mb-12 opacity-80 max-w-2xl mx-auto">
|
||||
For inquiries about sessions, art commissions, or event collaborations,
|
||||
please reach out through the form below or connect on social media.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{status === 'success' ? (
|
||||
<div className="max-w-lg mx-auto text-center py-12">
|
||||
<div className="text-5xl mb-6 text-[var(--accent-gold)]">✓</div>
|
||||
<h3 className="text-2xl font-light mb-4">Message Sent</h3>
|
||||
<p className="text-lg opacity-80 mb-8">
|
||||
Thank you for reaching out. I will get back to you soon.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setStatus('idle')}
|
||||
className="btn-outline"
|
||||
>
|
||||
Send Another Message
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="max-w-lg mx-auto text-left">
|
||||
<div className="mb-6">
|
||||
<label className="block font-sans-alt text-xs tracking-widest mb-2" htmlFor="name">
|
||||
NAME
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-4 py-3 border border-gray-200 rounded-lg focus:outline-none focus:border-[var(--accent-gold)] transition-colors"
|
||||
placeholder="Your name"
|
||||
disabled={status === 'loading'}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label className="block font-sans-alt text-xs tracking-widest mb-2" htmlFor="email">
|
||||
EMAIL
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full px-4 py-3 border border-gray-200 rounded-lg focus:outline-none focus:border-[var(--accent-gold)] transition-colors"
|
||||
placeholder="your@email.com"
|
||||
disabled={status === 'loading'}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label className="block font-sans-alt text-xs tracking-widest mb-2" htmlFor="message">
|
||||
MESSAGE
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
rows={5}
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
className="w-full px-4 py-3 border border-gray-200 rounded-lg focus:outline-none focus:border-[var(--accent-gold)] transition-colors resize-none"
|
||||
placeholder="How can I help?"
|
||||
disabled={status === 'loading'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{status === 'error' && errorMessage && (
|
||||
<p className="text-red-600 text-sm mb-4 font-sans-alt">{errorMessage}</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-filled w-full mt-4"
|
||||
disabled={status === 'loading'}
|
||||
style={status === 'loading' ? { opacity: 0.7, cursor: 'not-allowed' } : undefined}
|
||||
>
|
||||
{status === 'loading' ? 'Sending...' : 'Send Message'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Social Connect Section
|
||||
function SocialSection() {
|
||||
const instagramPosts = [
|
||||
{ image: '/images/art/soul-agreement.webp', alt: 'Soul Agreement' },
|
||||
{ image: '/images/art/goddess.webp?v=2', alt: 'Goddess' },
|
||||
{ image: '/images/art/twin-flames.webp', alt: 'Twin Flames' },
|
||||
{ image: '/images/art/featured.jpg?v=2', alt: 'Featured Art' },
|
||||
{ image: '/images/about/portrait-1.jpg', alt: 'Portrait' },
|
||||
{ image: '/images/reevolution/dj-xhiva.jpg', alt: 'DJ XHIVA' },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="section bg-[var(--bg-cream)]">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
FOLLOW THE JOURNEY
|
||||
</p>
|
||||
<h2 className="text-4xl md:text-5xl font-light mb-6">
|
||||
Connect on Socials
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 mb-10 max-w-4xl mx-auto">
|
||||
{instagramPosts.map((post, index) => (
|
||||
<a
|
||||
key={index}
|
||||
href="https://www.instagram.com/xhiva_art"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group bg-white rounded-xl shadow-md hover:shadow-xl transition-shadow duration-300 overflow-hidden border border-gray-100"
|
||||
>
|
||||
<div className="flex items-center gap-2 px-3 py-2.5">
|
||||
<div className="w-7 h-7 rounded-full bg-gradient-to-br from-[#f09433] via-[#e6683c] to-[#bc1888] p-[2px]">
|
||||
<div className="w-full h-full rounded-full bg-white p-[1px]">
|
||||
<div className="relative w-full h-full rounded-full overflow-hidden">
|
||||
<Image src="/images/about/portrait-1.jpg" alt="xhiva_art" fill className="object-cover object-[30%_center]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-[var(--text-dark)]">xhiva_art</span>
|
||||
</div>
|
||||
<div className="relative aspect-square">
|
||||
<Image
|
||||
src={post.image}
|
||||
alt={post.alt}
|
||||
fill
|
||||
className="object-cover"
|
||||
style={post.image.includes('portrait-1') ? { objectPosition: '30% center' } : post.image.includes('dj-xhiva') ? { objectPosition: 'center top' } : undefined}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-3 py-2.5 flex items-center gap-4">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[var(--text-dark)] group-hover:text-red-500 transition-colors">
|
||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
|
||||
</svg>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[var(--text-dark)]">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[var(--text-dark)]">
|
||||
<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row justify-center items-center gap-4">
|
||||
<a
|
||||
href="https://www.instagram.com/xhiva_art"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-filled"
|
||||
>
|
||||
Follow @xhiva_art on Instagram
|
||||
</a>
|
||||
<a
|
||||
href="https://www.facebook.com/XimenaXhivart"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-outline"
|
||||
>
|
||||
Follow on Facebook
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Footer
|
||||
function Footer() {
|
||||
return (
|
||||
<footer className="dark-section py-16 px-6">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="grid md:grid-cols-4 gap-12 mb-12">
|
||||
<div className="md:col-span-2">
|
||||
<h3 className="text-2xl font-light tracking-widest mb-4">XHIVA ART</h3>
|
||||
<p className="text-sm opacity-70 leading-relaxed mb-6">
|
||||
Visionary Art · Ceremony · Cultural Experiences
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-sans-alt text-xs tracking-widest mb-4">EXPLORE</h4>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Link href="#art" className="footer-link">Art</Link>
|
||||
<Link href="#services" className="footer-link">Services</Link>
|
||||
<Link href="#reevolution" className="footer-link">Re Evolution Art</Link>
|
||||
<Link href="/about" className="footer-link">About</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-sans-alt text-xs tracking-widest mb-4">CONNECT</h4>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Link href="#contact" className="footer-link">Contact</Link>
|
||||
<a href="https://instagram.com/ximena_xaguar" target="_blank" rel="noopener noreferrer" className="footer-link">
|
||||
@ximena_xaguar
|
||||
</a>
|
||||
<a href="https://www.facebook.com/XimenaXhivart" target="_blank" rel="noopener noreferrer" className="footer-link">
|
||||
Facebook
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-white/10 pt-8 text-center">
|
||||
<p className="font-sans-alt text-xs tracking-widest opacity-50">
|
||||
© {new Date().getFullYear()} XHIVA ART. ALL RIGHTS RESERVED.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
// Main Client Component
|
||||
export default function HomeClient({
|
||||
artworks,
|
||||
events,
|
||||
services,
|
||||
}: {
|
||||
artworks: Artwork[];
|
||||
events: ArtEvent[];
|
||||
services: Service[];
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Navigation />
|
||||
<main>
|
||||
<HeroSection />
|
||||
<GallerySection artworks={artworks} />
|
||||
<ServicesSection services={services} />
|
||||
<ReEvolutionSection events={events} />
|
||||
<TestimonialsSection />
|
||||
<WorkWithMeSection />
|
||||
<ContactSection />
|
||||
<SocialSection />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
952
src/app/page.tsx
952
src/app/page.tsx
|
|
@ -1,948 +1,12 @@
|
|||
'use client';
|
||||
import { getArtworks, getEvents, getServices } from '@/lib/data';
|
||||
import HomeClient from './home-client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
// Navigation Component
|
||||
function Navigation() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className="fixed top-0 left-0 right-0 z-50 glass">
|
||||
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
|
||||
<Link href="/" className="text-2xl font-light tracking-widest">
|
||||
XHIVA ART
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<Link href="/" className="nav-link">Home</Link>
|
||||
<Link href="#art" className="nav-link">Art</Link>
|
||||
<Link href="#about" className="nav-link">About</Link>
|
||||
<Link href="#services" className="nav-link">Services</Link>
|
||||
<Link href="#reevolution" className="nav-link">Re Evolution Art</Link>
|
||||
<Link href="#contact" className="nav-link">Contact</Link>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<button
|
||||
className="md:hidden p-2"
|
||||
onClick={() => setMobileMenuOpen(true)}
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<line x1="3" y1="6" x2="21" y2="6"/>
|
||||
<line x1="3" y1="12" x2="21" y2="12"/>
|
||||
<line x1="3" y1="18" x2="21" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="mobile-menu">
|
||||
<button
|
||||
className="absolute top-6 right-6 p-2"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
<Link href="/" className="nav-link text-xl" onClick={() => setMobileMenuOpen(false)}>Home</Link>
|
||||
<Link href="#art" className="nav-link text-xl" onClick={() => setMobileMenuOpen(false)}>Art</Link>
|
||||
<Link href="#about" className="nav-link text-xl" onClick={() => setMobileMenuOpen(false)}>About</Link>
|
||||
<Link href="#services" className="nav-link text-xl" onClick={() => setMobileMenuOpen(false)}>Services</Link>
|
||||
<Link href="#reevolution" className="nav-link text-xl" onClick={() => setMobileMenuOpen(false)}>Re Evolution Art</Link>
|
||||
<Link href="#contact" className="nav-link text-xl" onClick={() => setMobileMenuOpen(false)}>Contact</Link>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Hero Section
|
||||
function HeroSection() {
|
||||
return (
|
||||
<section className="min-h-screen relative flex flex-col items-center justify-center text-center px-6 pt-20">
|
||||
{/* Background Image */}
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src="/images/hero/hero-main.jpg"
|
||||
alt="Ximena Xaguar"
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-[var(--bg-cream)]/90 via-[var(--bg-cream)]/70 to-[var(--bg-cream)]/90" />
|
||||
</div>
|
||||
|
||||
<div className="max-w-4xl mx-auto relative z-10">
|
||||
<p className="font-sans-alt text-xs tracking-[0.3em] text-[var(--text-muted)] mb-6 fade-in">
|
||||
MULTIDISCIPLINARY VISIONARY ARTIST
|
||||
</p>
|
||||
<h1 className="text-5xl md:text-7xl lg:text-8xl font-light tracking-wider mb-8 fade-in stagger-1">
|
||||
XIMENA XAGUAR
|
||||
</h1>
|
||||
<div className="divider fade-in stagger-2"></div>
|
||||
<p className="text-lg md:text-xl max-w-2xl mx-auto mb-16 leading-relaxed opacity-80 fade-in stagger-2">
|
||||
Working at the intersection of art, ritual and embodied presence, creating experiences
|
||||
that support clarity, integration, exploration and transformation.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center fade-in stagger-3">
|
||||
<Link href="#art" className="btn-outline">
|
||||
Explore Art & Ritual Sessions
|
||||
</Link>
|
||||
<Link href="#reevolution" className="btn-filled">
|
||||
Discover Re Evolution Art
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scroll indicator */}
|
||||
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 animate-bounce opacity-50 z-10">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M12 5v14M5 12l7 7 7-7"/>
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Ritual Art Alchemy Section
|
||||
function RitualArtSection() {
|
||||
return (
|
||||
<section id="art" className="section bg-white">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="grid md:grid-cols-2 gap-12 items-center">
|
||||
{/* Featured Art Image */}
|
||||
<div className="relative aspect-[3/4] rounded-2xl overflow-hidden shadow-xl">
|
||||
<Image
|
||||
src="/images/art/featured.jpg?v=2"
|
||||
alt="Visionary Art by Ximena Xaguar"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
VISUAL ALCHEMY
|
||||
</p>
|
||||
<h2 className="text-4xl md:text-5xl font-light mb-6">
|
||||
Ritual Art Alchemy
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
<p className="text-lg leading-relaxed mb-6 opacity-80">
|
||||
My art is my ritual — a journey of transformation. Each painting emerges from
|
||||
cycles of death and rebirth, shadow work, intuitive vision and ancestral memory.
|
||||
</p>
|
||||
<p className="text-lg leading-relaxed mb-12 opacity-80">
|
||||
Through symbolic language and universal cosmovision, I translate inner processes
|
||||
into living images. These artworks are not objects. They are portals. Allies for
|
||||
emotional integration, transformation, spiritual insight and energetic coherence.
|
||||
</p>
|
||||
<div className="pt-2">
|
||||
<Link href="#gallery" className="btn-outline">
|
||||
Enter the Gallery
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Art Gallery Section
|
||||
function GallerySection() {
|
||||
const [selectedArtwork, setSelectedArtwork] = useState<{ src: string; title: string } | null>(null);
|
||||
|
||||
const artworks = [
|
||||
{ src: '/images/art/goddess.webp?v=2', title: 'Goddess' },
|
||||
{ src: '/images/art/mujer-medicina.webp', title: 'Mujer Medicina' },
|
||||
{ src: '/images/art/shiva-main.webp', title: 'Shiva' },
|
||||
{ src: '/images/art/twin-flames.webp', title: 'Twin Flames' },
|
||||
{ src: '/images/art/soul-agreement.webp', title: 'Soul Agreement' },
|
||||
{ src: '/images/art/madre.webp', title: 'Madre' },
|
||||
{ src: '/images/art/warmy-munachi.webp', title: 'Warmy Munachi' },
|
||||
{ src: '/images/art/mi-uma.webp', title: 'Mi Uma' },
|
||||
{ src: '/images/art/re-evolucion-arte.webp', title: 'Re Evolución Arte' },
|
||||
{ src: '/images/art/mujer-ser.webp', title: 'Mujer Ser' },
|
||||
{ src: '/images/art/arte-parque.webp', title: 'Arte Parque' },
|
||||
{ src: '/images/art/rainbow-tara.webp', title: 'Rainbow Tara' },
|
||||
{ src: '/images/art/female-fire.webp', title: 'Female Fire' },
|
||||
{ src: '/images/art/trinidad.webp', title: 'Trinidad' },
|
||||
{ src: '/images/art/tantra.webp', title: 'Tantra' },
|
||||
{ src: '/images/art/amazonas.webp', title: 'Amazonas' },
|
||||
{ src: '/images/art/mujer-jaguar.webp', title: 'Mujer Jaguar' },
|
||||
{ src: '/images/art/coming-from-the-darkness.webp', title: 'Coming from the Darkness' },
|
||||
{ src: '/images/art/white-tara.webp', title: 'White Tara' },
|
||||
{ src: '/images/art/aya.webp', title: 'Aya' },
|
||||
{ src: '/images/art/pacha.webp', title: 'Pacha' },
|
||||
{ src: '/images/art/pacha-detalle.webp', title: 'Pacha (Detalle)' },
|
||||
{ src: '/images/art/warmy-arkanum.webp', title: 'Warmy Arkanum' },
|
||||
{ src: '/images/art/sacro.webp', title: 'Sacro' },
|
||||
{ src: '/images/art/inti-om.webp', title: 'Inti Om' },
|
||||
{ src: '/images/art/totem-astral.webp', title: 'Totem Astral' },
|
||||
{ src: '/images/art/sabiduria-ancestral.webp', title: 'Sabiduría Ancestral' },
|
||||
{ src: '/images/art/amor-astral.webp', title: 'Amor Astral' },
|
||||
{ src: '/images/art/escencia-mistica.webp', title: 'Escencia Mística' },
|
||||
{ src: '/images/art/raiz.webp', title: 'Raíz' },
|
||||
{ src: '/images/art/uni-verso.webp', title: 'Uni Verso' },
|
||||
{ src: '/images/art/wayra.webp', title: 'Wayra' },
|
||||
{ src: '/images/art/la-illimani.webp', title: 'La Illimani' },
|
||||
{ src: '/images/about/process-1.webp', title: 'Eagle & Jaguar' },
|
||||
{ src: '/images/about/process-2.webp', title: 'Kundalini' },
|
||||
{ src: '/images/about/process-3.webp', title: 'Ancestral Totem' },
|
||||
{ src: '/images/about/process-5.webp', title: 'Detail — Fire' },
|
||||
{ src: '/images/about/process-6.webp', title: 'Sacred Union' },
|
||||
{ src: '/images/about/process-7.webp', title: 'Shiva II' },
|
||||
{ src: '/images/about/portrait-artist-2.webp', title: 'Golden Meditation' },
|
||||
];
|
||||
|
||||
return (
|
||||
<section id="gallery" className="section bg-[var(--bg-cream)]">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
GALLERY
|
||||
</p>
|
||||
<h2 className="text-4xl md:text-5xl font-light mb-6">
|
||||
Visionary Artworks
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{artworks.map((artwork, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="relative aspect-square rounded-lg overflow-hidden group cursor-pointer"
|
||||
onClick={() => setSelectedArtwork(artwork)}
|
||||
>
|
||||
<Image
|
||||
src={artwork.src}
|
||||
alt={artwork.title}
|
||||
fill
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-110"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-colors duration-300 flex items-end justify-center pb-4">
|
||||
<span className="text-white opacity-0 group-hover:opacity-100 transition-opacity duration-300 font-sans-alt text-xs tracking-widest">
|
||||
{artwork.title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lightbox Modal */}
|
||||
{selectedArtwork && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/90 p-4 md:p-8"
|
||||
onClick={() => setSelectedArtwork(null)}
|
||||
>
|
||||
<button
|
||||
className="absolute top-4 right-4 md:top-6 md:right-6 z-50 text-white/70 hover:text-white transition-colors p-2"
|
||||
onClick={() => setSelectedArtwork(null)}
|
||||
aria-label="Close"
|
||||
>
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div className="relative w-full h-full max-w-6xl max-h-[85vh] flex flex-col items-center justify-center">
|
||||
<div
|
||||
className="relative w-full flex-1 min-h-0"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Image
|
||||
src={selectedArtwork.src}
|
||||
alt={selectedArtwork.title}
|
||||
fill
|
||||
className="object-contain"
|
||||
sizes="(max-width: 768px) 100vw, 90vw"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
className="text-white/80 font-sans-alt text-sm tracking-widest mt-4 text-center"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{selectedArtwork.title}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Services Section
|
||||
function ServicesSection() {
|
||||
const services = [
|
||||
{
|
||||
title: 'Crystal Therapy',
|
||||
subtitle: 'Nervous System Alignment',
|
||||
duration: 'Crystal reading / 1 hour',
|
||||
description: 'One-to-one sessions supporting nervous system regulation and emotional integration through crystal energetic work, elemental somatic presence and intuitive mapping addressing emotional, physical, mental and spiritual coherence.',
|
||||
color: 'lavender',
|
||||
image: '/images/services/crystal-therapy.webp',
|
||||
},
|
||||
{
|
||||
title: 'Temazcal',
|
||||
subtitle: 'Medicine of the Four Elements',
|
||||
duration: 'Fire - Earth - Water - Air',
|
||||
description: 'Sweatlodge ceremonies guided for 15+ years, rooted in Native American ancestral tradition. Working with four elements and four bodies (physical, emotional, mental, spiritual) within a contained ritual space supporting purification, closure and renewal.',
|
||||
color: 'mint',
|
||||
image: '/images/services/temazcal.jpg',
|
||||
},
|
||||
{
|
||||
title: 'Deep Integration',
|
||||
subtitle: 'Transformation Session',
|
||||
duration: '2 Hours',
|
||||
description: 'Tailored container for life transitions, post-ceremonial integration or emotional transformation combining somatic work, crystal mapping, astrological and ritual guidance supporting embodiment of insight and conscious transformation.',
|
||||
color: 'rose',
|
||||
image: '/images/services/deep-integration.webp',
|
||||
},
|
||||
{
|
||||
title: 'Soul Portrait',
|
||||
subtitle: 'Art Alchemy',
|
||||
duration: 'Group Workshops · TRIBAL Experience',
|
||||
description: 'Weaving visionary art with therapeutic process through guided creative immersion. In group workshop settings, participants access deeper self-expression, emotional release and soul integration.',
|
||||
color: 'pink',
|
||||
image: '/images/art/soul-agreement.webp',
|
||||
highlighted: true,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section id="services" className="section">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
OFFERINGS
|
||||
</p>
|
||||
<h2 className="text-4xl md:text-5xl font-light mb-6">
|
||||
Ritual Healing & Integration
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
<p className="text-lg max-w-2xl mx-auto opacity-80">
|
||||
Decades of embodied practice since 2009. Current focus is integration and embodiment—
|
||||
supporting people to anchor insight, develop inner coherence and regulate nervous systems
|
||||
within deep transformation processes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
{services.map((service, index) => (
|
||||
<div key={index} className={`group ${service.highlighted ? 'ring-2 ring-[var(--accent-gold)] rounded-xl' : ''}`}>
|
||||
<div className="relative aspect-[4/3] rounded-t-xl overflow-hidden">
|
||||
<Image
|
||||
src={service.image}
|
||||
alt={service.title}
|
||||
fill
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
<div className={`service-card ${service.color} rounded-t-none`}>
|
||||
<h3 className="text-2xl font-light mb-2">{service.title}</h3>
|
||||
<p className="font-sans-alt text-xs tracking-widest text-[var(--text-muted)] mb-1">
|
||||
{service.subtitle}
|
||||
</p>
|
||||
<p className="font-sans-alt text-xs tracking-widest text-[var(--accent-gold)] mb-6">
|
||||
{service.duration}
|
||||
</p>
|
||||
<p className="text-sm leading-relaxed opacity-80">
|
||||
{service.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mt-20">
|
||||
<a href="https://booking.xhiva.art" target="_blank" rel="noopener noreferrer" className="btn-outline">
|
||||
Book a Session
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Re Evolution Art Section
|
||||
function ReEvolutionSection() {
|
||||
const events = [
|
||||
{
|
||||
title: 'Tribal Nights',
|
||||
description: 'A signature event weaving ritual, dance and immersive art into one transformative experience.',
|
||||
image: '/images/reevolution/tribal-night.jpg',
|
||||
},
|
||||
{
|
||||
title: 'Tribal Experience',
|
||||
description: 'Group immersion exploring the unconscious through shamanic journeying, art alchemy and embodied movement.',
|
||||
image: '/images/reevolution/event-2.jpg',
|
||||
},
|
||||
{
|
||||
title: 'Visionary Art Week Zürich',
|
||||
description: 'Curated week featuring international artists, performances, live music, talks and workshops.',
|
||||
image: '/images/reevolution/event-3.jpg',
|
||||
},
|
||||
{
|
||||
title: 'Future Gatherings',
|
||||
description: 'Experimental experiences weaving DJ sets with expanded states of presence.',
|
||||
image: '/images/reevolution/dj-xhiva.jpg',
|
||||
objectPosition: 'center top',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section id="reevolution" className="section dark-section">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
{/* Logo */}
|
||||
<div className="relative w-full max-w-96 aspect-[3/2] mx-auto mb-2 overflow-hidden">
|
||||
<Image
|
||||
src="/images/reevolution/logo.png"
|
||||
alt="Re Evolution Art Logo"
|
||||
fill
|
||||
className="object-contain object-center scale-150"
|
||||
/>
|
||||
</div>
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
CULTURAL PLATFORM
|
||||
</p>
|
||||
<h2 style={{ fontFamily: "'Krown', sans-serif", fontWeight: 700 }} className="text-4xl md:text-5xl mb-6">
|
||||
Re Evolution Art
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-lg max-w-3xl mx-auto opacity-80">
|
||||
A visionary cultural platform based in Switzerland and rooted in Bolivia. Through exhibitions,
|
||||
immersive gatherings, TRIBAL events and collaborative projects, we bring together artists,
|
||||
ritualists, musicians and independent creators engaged in inner work and cultural dialogue.
|
||||
We bridge ancestral memory with contemporary expression.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{events.map((event, index) => (
|
||||
<div key={index} className="event-card group overflow-hidden">
|
||||
<div className="relative aspect-video rounded-lg overflow-hidden mb-4">
|
||||
<Image
|
||||
src={event.image}
|
||||
alt={event.title}
|
||||
fill
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
style={event.objectPosition ? { objectPosition: event.objectPosition } : undefined}
|
||||
/>
|
||||
</div>
|
||||
<h3 style={{ fontFamily: "'Krown', sans-serif", fontWeight: 700 }} className="text-xl mb-3 text-[var(--accent-gold)]">
|
||||
{event.title}
|
||||
</h3>
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-sm opacity-70 leading-relaxed">
|
||||
{event.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Social Links */}
|
||||
<div className="flex justify-center gap-6 mt-16">
|
||||
<a
|
||||
href="https://www.instagram.com/reevolutionart"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-outline btn-outline-light"
|
||||
style={{ fontFamily: "'Narrenschiff', sans-serif" }}
|
||||
>
|
||||
Instagram
|
||||
</a>
|
||||
<a
|
||||
href="https://www.facebook.com/groups/383432167715274/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-outline btn-outline-light"
|
||||
style={{ fontFamily: "'Narrenschiff', sans-serif" }}
|
||||
>
|
||||
Facebook Community
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Testimonials Section
|
||||
function TestimonialsSection() {
|
||||
const testimonials = [
|
||||
{
|
||||
quote: "The crystal healing session created an immediate connection and brought my soul forth. I deeply appreciate the support through the process.",
|
||||
author: "Dieter Katterbach",
|
||||
},
|
||||
{
|
||||
quote: "The sweat lodge ceremony was powerfully moving, highlighting the integration of healing, compassion and authentic expression.",
|
||||
author: "Kermit Goodman",
|
||||
},
|
||||
{
|
||||
quote: "The experience was powerfully transformative — it changed my understanding of ceremony itself.",
|
||||
author: "Verana Bailowitz",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="section bg-white">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
TESTIMONIALS
|
||||
</p>
|
||||
<h2 className="text-4xl md:text-5xl font-light mb-6">
|
||||
Words of Gratitude
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8">
|
||||
{testimonials.map((testimonial, index) => (
|
||||
<div key={index} className="testimonial-card">
|
||||
<span className="quote-mark">"</span>
|
||||
<p className="testimonial mb-6">
|
||||
{testimonial.quote}
|
||||
</p>
|
||||
<p className="font-sans-alt text-xs tracking-widest text-[var(--accent-gold)]">
|
||||
— {testimonial.author}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// About Section
|
||||
function AboutSection() {
|
||||
return (
|
||||
<section id="about" className="section">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="grid md:grid-cols-2 gap-12 items-center">
|
||||
<div className="text-center order-2 md:order-1">
|
||||
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
THE ARTIST
|
||||
</p>
|
||||
<h2 className="text-4xl md:text-5xl font-light mb-6">
|
||||
About Ximena
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
<p className="text-lg leading-relaxed mb-6 opacity-80">
|
||||
Years dedicated to work in Bolivia, Peru and Switzerland shaped this path through
|
||||
emotional integration, somatic healing and ceremonial support.
|
||||
</p>
|
||||
<p className="text-lg leading-relaxed mb-12 opacity-80">
|
||||
A beautiful, talented artist rooted in ancestral culture and land connection —
|
||||
healer, visionary artist, mother and friend — providing deep support through inner work.
|
||||
</p>
|
||||
<div className="pt-2">
|
||||
<Link href="#contact" className="btn-outline">
|
||||
Connect
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Portrait */}
|
||||
<div className="relative aspect-[3/4] rounded-2xl overflow-hidden shadow-xl order-1 md:order-2">
|
||||
<Image
|
||||
src="/images/about/portrait-1.jpg"
|
||||
alt="Ximena Xaguar"
|
||||
fill
|
||||
className="object-cover object-[30%_center]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Art in Motion */}
|
||||
<div className="mt-16 text-center">
|
||||
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
THE PROCESS
|
||||
</p>
|
||||
<h3 className="text-3xl md:text-4xl font-light mb-6">
|
||||
Art in Motion
|
||||
</h3>
|
||||
<div className="divider"></div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3 mt-8">
|
||||
{[
|
||||
{ src: '/images/about/painting-process.webp', alt: 'Ximena painting', position: 'top' },
|
||||
{ src: '/images/art/mural-bio-centro.webp', alt: 'Mural Bio Centro Guembe' },
|
||||
{ src: '/images/about/portrait-2.jpg', alt: 'Ximena painting mural' },
|
||||
{ src: '/images/about/portrait-3.jpg', alt: 'Ximena in studio', position: 'top' },
|
||||
{ src: '/images/about/portrait-4.jpg', alt: 'Ximena at ceremony' },
|
||||
{ src: '/images/about/pachamama.jpg', alt: 'Ximena — ritual with smoke' },
|
||||
].map((photo, index) => (
|
||||
<div key={index} className="relative aspect-[4/3] rounded-lg overflow-hidden group">
|
||||
<Image
|
||||
src={photo.src}
|
||||
alt={photo.alt}
|
||||
fill
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
style={photo.position ? { objectPosition: photo.position } : undefined}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Work With Me Section
|
||||
function WorkWithMeSection() {
|
||||
return (
|
||||
<section className="section relative overflow-hidden">
|
||||
{/* Background Image */}
|
||||
<div className="absolute inset-0 z-0 bg-[#0f0f0f]">
|
||||
<Image
|
||||
src="/images/art/wayra-bg.webp"
|
||||
alt="Wayra"
|
||||
fill
|
||||
className="object-contain opacity-40"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="max-w-3xl mx-auto relative z-10">
|
||||
<div className="text-center">
|
||||
<h2 className="text-4xl md:text-5xl font-light mb-6 text-[var(--text-light)]">
|
||||
Work With Me
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
<p className="text-lg leading-relaxed mb-16 opacity-80 max-w-2xl mx-auto text-[var(--text-light)]">
|
||||
This work is for those ready to meet themselves honestly with courage,
|
||||
sensitivity and presence.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||
<a href="https://booking.xhiva.art" target="_blank" rel="noopener noreferrer" className="btn-outline btn-outline-light">
|
||||
Book a Session
|
||||
</a>
|
||||
<Link href="#contact" className="btn-filled" style={{ background: 'var(--accent-gold)', borderColor: 'var(--accent-gold)' }}>
|
||||
Contact Me
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Contact Section
|
||||
function ContactSection() {
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!name.trim() || !email.trim() || !message.trim()) {
|
||||
setStatus('error');
|
||||
setErrorMessage('Please fill in all fields.');
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus('loading');
|
||||
setErrorMessage('');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: name.trim(), email: email.trim(), message: message.trim() }),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || 'Something went wrong');
|
||||
}
|
||||
|
||||
setStatus('success');
|
||||
setName('');
|
||||
setEmail('');
|
||||
setMessage('');
|
||||
} catch (err) {
|
||||
setStatus('error');
|
||||
setErrorMessage(err instanceof Error ? err.message : 'Failed to send message. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="contact" className="section bg-white">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="text-center">
|
||||
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
GET IN TOUCH
|
||||
</p>
|
||||
<h2 className="text-4xl md:text-5xl font-light mb-6">
|
||||
Begin Your Journey
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
<p className="text-lg leading-relaxed mb-12 opacity-80 max-w-2xl mx-auto">
|
||||
For inquiries about sessions, art commissions, or event collaborations,
|
||||
please reach out through the form below or connect on social media.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{status === 'success' ? (
|
||||
<div className="max-w-lg mx-auto text-center py-12">
|
||||
<div className="text-5xl mb-6 text-[var(--accent-gold)]">✓</div>
|
||||
<h3 className="text-2xl font-light mb-4">Message Sent</h3>
|
||||
<p className="text-lg opacity-80 mb-8">
|
||||
Thank you for reaching out. I will get back to you soon.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setStatus('idle')}
|
||||
className="btn-outline"
|
||||
>
|
||||
Send Another Message
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="max-w-lg mx-auto text-left">
|
||||
<div className="mb-6">
|
||||
<label className="block font-sans-alt text-xs tracking-widest mb-2" htmlFor="name">
|
||||
NAME
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-4 py-3 border border-gray-200 rounded-lg focus:outline-none focus:border-[var(--accent-gold)] transition-colors"
|
||||
placeholder="Your name"
|
||||
disabled={status === 'loading'}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label className="block font-sans-alt text-xs tracking-widest mb-2" htmlFor="email">
|
||||
EMAIL
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full px-4 py-3 border border-gray-200 rounded-lg focus:outline-none focus:border-[var(--accent-gold)] transition-colors"
|
||||
placeholder="your@email.com"
|
||||
disabled={status === 'loading'}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label className="block font-sans-alt text-xs tracking-widest mb-2" htmlFor="message">
|
||||
MESSAGE
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
rows={5}
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
className="w-full px-4 py-3 border border-gray-200 rounded-lg focus:outline-none focus:border-[var(--accent-gold)] transition-colors resize-none"
|
||||
placeholder="How can I support your journey?"
|
||||
disabled={status === 'loading'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{status === 'error' && errorMessage && (
|
||||
<p className="text-red-600 text-sm mb-4 font-sans-alt">{errorMessage}</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-filled w-full mt-4"
|
||||
disabled={status === 'loading'}
|
||||
style={status === 'loading' ? { opacity: 0.7, cursor: 'not-allowed' } : undefined}
|
||||
>
|
||||
{status === 'loading' ? 'Sending...' : 'Send Message'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Social Connect Section
|
||||
function SocialSection() {
|
||||
const instagramPosts = [
|
||||
{ image: '/images/art/soul-agreement.webp', alt: 'Soul Agreement' },
|
||||
{ image: '/images/art/goddess.webp?v=2', alt: 'Goddess' },
|
||||
{ image: '/images/art/twin-flames.webp', alt: 'Twin Flames' },
|
||||
{ image: '/images/art/featured.jpg?v=2', alt: 'Featured Art' },
|
||||
{ image: '/images/about/portrait-1.jpg', alt: 'Portrait' },
|
||||
{ image: '/images/reevolution/dj-xhiva.jpg', alt: 'DJ XHIVA' },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="section bg-[var(--bg-cream)]">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
FOLLOW THE JOURNEY
|
||||
</p>
|
||||
<h2 className="text-4xl md:text-5xl font-light mb-6">
|
||||
Connect on Socials
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
</div>
|
||||
|
||||
{/* Instagram-style Grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 mb-10 max-w-4xl mx-auto">
|
||||
{instagramPosts.map((post, index) => (
|
||||
<a
|
||||
key={index}
|
||||
href="https://www.instagram.com/xhiva_art"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group bg-white rounded-xl shadow-md hover:shadow-xl transition-shadow duration-300 overflow-hidden border border-gray-100"
|
||||
>
|
||||
{/* Post Header */}
|
||||
<div className="flex items-center gap-2 px-3 py-2.5">
|
||||
<div className="w-7 h-7 rounded-full bg-gradient-to-br from-[#f09433] via-[#e6683c] to-[#bc1888] p-[2px]">
|
||||
<div className="w-full h-full rounded-full bg-white p-[1px]">
|
||||
<div className="relative w-full h-full rounded-full overflow-hidden">
|
||||
<Image src="/images/about/portrait-1.jpg" alt="xhiva_art" fill className="object-cover object-[30%_center]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-[var(--text-dark)]">xhiva_art</span>
|
||||
</div>
|
||||
{/* Post Image */}
|
||||
<div className="relative aspect-square">
|
||||
<Image
|
||||
src={post.image}
|
||||
alt={post.alt}
|
||||
fill
|
||||
className="object-cover"
|
||||
style={post.image.includes('portrait-1') ? { objectPosition: '30% center' } : post.image.includes('dj-xhiva') ? { objectPosition: 'center top' } : undefined}
|
||||
/>
|
||||
</div>
|
||||
{/* Post Actions */}
|
||||
<div className="px-3 py-2.5 flex items-center gap-4">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[var(--text-dark)] group-hover:text-red-500 transition-colors">
|
||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
|
||||
</svg>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[var(--text-dark)]">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[var(--text-dark)]">
|
||||
<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Social Buttons */}
|
||||
<div className="flex flex-col sm:flex-row justify-center items-center gap-4">
|
||||
<a
|
||||
href="https://www.instagram.com/xhiva_art"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-filled"
|
||||
>
|
||||
Follow @xhiva_art on Instagram
|
||||
</a>
|
||||
<a
|
||||
href="https://www.facebook.com/XimenaXhivart"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-outline"
|
||||
>
|
||||
Follow on Facebook
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Footer
|
||||
function Footer() {
|
||||
return (
|
||||
<footer className="dark-section py-16 px-6">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="grid md:grid-cols-4 gap-12 mb-12">
|
||||
{/* Brand */}
|
||||
<div className="md:col-span-2">
|
||||
<h3 className="text-2xl font-light tracking-widest mb-4">XHIVA ART</h3>
|
||||
<p className="text-sm opacity-70 leading-relaxed mb-6">
|
||||
Visionary Art · Ritual Healing · Cultural and Artistic Experiences
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Links */}
|
||||
<div>
|
||||
<h4 className="font-sans-alt text-xs tracking-widest mb-4">EXPLORE</h4>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Link href="#art" className="footer-link">Art</Link>
|
||||
<Link href="#gallery" className="footer-link">Gallery</Link>
|
||||
<Link href="#about" className="footer-link">About</Link>
|
||||
<Link href="#services" className="footer-link">Services</Link>
|
||||
<Link href="#reevolution" className="footer-link">Re Evolution Art</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Connect */}
|
||||
<div>
|
||||
<h4 className="font-sans-alt text-xs tracking-widest mb-4">CONNECT</h4>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Link href="#contact" className="footer-link">Contact</Link>
|
||||
<a href="https://instagram.com/ximena_xaguar" target="_blank" rel="noopener noreferrer" className="footer-link">
|
||||
@ximena_xaguar
|
||||
</a>
|
||||
<a href="https://www.facebook.com/XimenaXhivart" target="_blank" rel="noopener noreferrer" className="footer-link">
|
||||
Facebook
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Copyright */}
|
||||
<div className="border-t border-white/10 pt-8 text-center">
|
||||
<p className="font-sans-alt text-xs tracking-widest opacity-50">
|
||||
© {new Date().getFullYear()} XHIVA ART. ALL RIGHTS RESERVED.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
// Main Page Component
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<Navigation />
|
||||
<main>
|
||||
<HeroSection />
|
||||
<RitualArtSection />
|
||||
<GallerySection />
|
||||
<ServicesSection />
|
||||
<ReEvolutionSection />
|
||||
<TestimonialsSection />
|
||||
<AboutSection />
|
||||
<WorkWithMeSection />
|
||||
<ContactSection />
|
||||
<SocialSection />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
const artworks = getArtworks();
|
||||
const events = getEvents();
|
||||
const services = getServices();
|
||||
|
||||
return <HomeClient artworks={artworks} events={events} services={services} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
const COOKIE_NAME = 'xhiva_session';
|
||||
const SESSION_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||
|
||||
function getPassword(): string {
|
||||
return process.env.ADMIN_PASSWORD || '';
|
||||
}
|
||||
|
||||
async function sign(timestamp: number): Promise<string> {
|
||||
const password = getPassword();
|
||||
const encoder = new TextEncoder();
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(password),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['sign']
|
||||
);
|
||||
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(String(timestamp)));
|
||||
return Array.from(new Uint8Array(signature))
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
export async function createSessionToken(): Promise<string> {
|
||||
const timestamp = Date.now();
|
||||
const sig = await sign(timestamp);
|
||||
return `${timestamp}.${sig}`;
|
||||
}
|
||||
|
||||
export async function verifySessionToken(token: string): Promise<boolean> {
|
||||
const password = getPassword();
|
||||
if (!password) return false;
|
||||
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 2) return false;
|
||||
|
||||
const timestamp = parseInt(parts[0], 10);
|
||||
if (isNaN(timestamp)) return false;
|
||||
|
||||
// Check expiry
|
||||
if (Date.now() - timestamp > SESSION_DURATION) return false;
|
||||
|
||||
// Check signature
|
||||
const expected = await sign(timestamp);
|
||||
return parts[1] === expected;
|
||||
}
|
||||
|
||||
export function validatePassword(input: string): boolean {
|
||||
const password = getPassword();
|
||||
if (!password) return false;
|
||||
return input === password;
|
||||
}
|
||||
|
||||
export function getSessionCookie(token: string): string {
|
||||
const maxAge = SESSION_DURATION / 1000;
|
||||
return `${COOKIE_NAME}=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAge}`;
|
||||
}
|
||||
|
||||
export function clearSessionCookie(): string {
|
||||
return `${COOKIE_NAME}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
|
||||
}
|
||||
|
||||
export function getTokenFromCookies(cookieHeader: string | null): string | null {
|
||||
if (!cookieHeader) return null;
|
||||
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${COOKIE_NAME}=([^;]*)`));
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import type { Artwork, ArtEvent, Service } from './types';
|
||||
import { seedArtworks, seedEvents, seedServices } from './seed';
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || '/data';
|
||||
|
||||
function ensureDir() {
|
||||
if (!existsSync(DATA_DIR)) {
|
||||
mkdirSync(DATA_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function readJSON<T>(filename: string, seedData: T[]): T[] {
|
||||
const filepath = join(DATA_DIR, filename);
|
||||
if (existsSync(filepath)) {
|
||||
try {
|
||||
return JSON.parse(readFileSync(filepath, 'utf-8'));
|
||||
} catch {
|
||||
return seedData;
|
||||
}
|
||||
}
|
||||
// Auto-seed on first read
|
||||
ensureDir();
|
||||
writeFileSync(filepath, JSON.stringify(seedData, null, 2));
|
||||
return seedData;
|
||||
}
|
||||
|
||||
function writeJSON<T>(filename: string, data: T[]) {
|
||||
ensureDir();
|
||||
writeFileSync(join(DATA_DIR, filename), JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
// Artworks
|
||||
export function getArtworks(): Artwork[] {
|
||||
return readJSON<Artwork>('artworks.json', seedArtworks);
|
||||
}
|
||||
|
||||
export function getArtwork(id: string): Artwork | undefined {
|
||||
return getArtworks().find(a => a.id === id);
|
||||
}
|
||||
|
||||
export function saveArtworks(artworks: Artwork[]) {
|
||||
writeJSON('artworks.json', artworks);
|
||||
}
|
||||
|
||||
export function addArtwork(artwork: Artwork) {
|
||||
const artworks = getArtworks();
|
||||
artworks.push(artwork);
|
||||
saveArtworks(artworks);
|
||||
return artwork;
|
||||
}
|
||||
|
||||
export function updateArtwork(id: string, updates: Partial<Artwork>): Artwork | null {
|
||||
const artworks = getArtworks();
|
||||
const index = artworks.findIndex(a => a.id === id);
|
||||
if (index === -1) return null;
|
||||
artworks[index] = { ...artworks[index], ...updates, id };
|
||||
saveArtworks(artworks);
|
||||
return artworks[index];
|
||||
}
|
||||
|
||||
export function deleteArtwork(id: string): boolean {
|
||||
const artworks = getArtworks();
|
||||
const filtered = artworks.filter(a => a.id !== id);
|
||||
if (filtered.length === artworks.length) return false;
|
||||
saveArtworks(filtered);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Events
|
||||
export function getEvents(): ArtEvent[] {
|
||||
return readJSON<ArtEvent>('events.json', seedEvents);
|
||||
}
|
||||
|
||||
export function getEvent(id: string): ArtEvent | undefined {
|
||||
return getEvents().find(e => e.id === id);
|
||||
}
|
||||
|
||||
export function saveEvents(events: ArtEvent[]) {
|
||||
writeJSON('events.json', events);
|
||||
}
|
||||
|
||||
export function addEvent(event: ArtEvent) {
|
||||
const events = getEvents();
|
||||
events.push(event);
|
||||
saveEvents(events);
|
||||
return event;
|
||||
}
|
||||
|
||||
export function updateEvent(id: string, updates: Partial<ArtEvent>): ArtEvent | null {
|
||||
const events = getEvents();
|
||||
const index = events.findIndex(e => e.id === id);
|
||||
if (index === -1) return null;
|
||||
events[index] = { ...events[index], ...updates, id };
|
||||
saveEvents(events);
|
||||
return events[index];
|
||||
}
|
||||
|
||||
export function deleteEvent(id: string): boolean {
|
||||
const events = getEvents();
|
||||
const filtered = events.filter(e => e.id !== id);
|
||||
if (filtered.length === events.length) return false;
|
||||
saveEvents(filtered);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Services
|
||||
export function getServices(): Service[] {
|
||||
return readJSON<Service>('services.json', seedServices);
|
||||
}
|
||||
|
||||
export function getService(id: string): Service | undefined {
|
||||
return getServices().find(s => s.id === id);
|
||||
}
|
||||
|
||||
export function saveServices(services: Service[]) {
|
||||
writeJSON('services.json', services);
|
||||
}
|
||||
|
||||
export function addService(service: Service) {
|
||||
const services = getServices();
|
||||
services.push(service);
|
||||
saveServices(services);
|
||||
return service;
|
||||
}
|
||||
|
||||
export function updateService(id: string, updates: Partial<Service>): Service | null {
|
||||
const services = getServices();
|
||||
const index = services.findIndex(s => s.id === id);
|
||||
if (index === -1) return null;
|
||||
services[index] = { ...services[index], ...updates, id };
|
||||
saveServices(services);
|
||||
return services[index];
|
||||
}
|
||||
|
||||
export function deleteService(id: string): boolean {
|
||||
const services = getServices();
|
||||
const filtered = services.filter(s => s.id !== id);
|
||||
if (filtered.length === services.length) return false;
|
||||
saveServices(filtered);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
import type { Artwork, ArtEvent, Service } from './types';
|
||||
|
||||
export const seedArtworks: Artwork[] = [
|
||||
{ id: 'goddess', title: 'Goddess', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/goddess.webp?v=2', category: 'available', featured: true, sortOrder: 1 },
|
||||
{ id: 'mujer-medicina', title: 'Mujer Medicina', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/mujer-medicina.webp', category: 'available', featured: true, sortOrder: 2 },
|
||||
{ id: 'shiva', title: 'Shiva', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/shiva-main.webp', category: 'available', featured: true, sortOrder: 3 },
|
||||
{ id: 'twin-flames', title: 'Twin Flames', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/twin-flames.webp', category: 'available', featured: false, sortOrder: 4 },
|
||||
{ id: 'soul-agreement', title: 'Soul Agreement', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/soul-agreement.webp', category: 'available', featured: false, sortOrder: 5 },
|
||||
{ id: 'madre', title: 'Madre', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/madre.webp', category: 'available', featured: false, sortOrder: 6 },
|
||||
{ id: 'warmy-munachi', title: 'Warmy Munachi', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/warmy-munachi.webp', category: 'available', featured: false, sortOrder: 7 },
|
||||
{ id: 'mi-uma', title: 'Mi Uma', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/mi-uma.webp', category: 'available', featured: false, sortOrder: 8 },
|
||||
{ id: 're-evolucion-arte', title: 'Re Evoluci\u00f3n Arte', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/re-evolucion-arte.webp', category: 'available', featured: false, sortOrder: 9 },
|
||||
{ id: 'mujer-ser', title: 'Mujer Ser', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/mujer-ser.webp', category: 'available', featured: false, sortOrder: 10 },
|
||||
{ id: 'arte-parque', title: 'Arte Parque', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/arte-parque.webp', category: 'available', featured: false, sortOrder: 11 },
|
||||
{ id: 'rainbow-tara', title: 'Rainbow Tara', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/rainbow-tara.webp', category: 'available', featured: false, sortOrder: 12 },
|
||||
{ id: 'female-fire', title: 'Female Fire', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/female-fire.webp', category: 'available', featured: false, sortOrder: 13 },
|
||||
{ id: 'trinidad', title: 'Trinidad', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/trinidad.webp', category: 'available', featured: false, sortOrder: 14 },
|
||||
{ id: 'tantra', title: 'Tantra', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/tantra.webp', category: 'available', featured: false, sortOrder: 15 },
|
||||
{ id: 'amazonas', title: 'Amazonas', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/amazonas.webp', category: 'available', featured: false, sortOrder: 16 },
|
||||
{ id: 'mujer-jaguar', title: 'Mujer Jaguar', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/mujer-jaguar.webp', category: 'available', featured: false, sortOrder: 17 },
|
||||
{ id: 'coming-from-the-darkness', title: 'Coming from the Darkness', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/coming-from-the-darkness.webp', category: 'available', featured: false, sortOrder: 18 },
|
||||
{ id: 'white-tara', title: 'White Tara', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/white-tara.webp', category: 'available', featured: false, sortOrder: 19 },
|
||||
{ id: 'aya', title: 'Aya', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/aya.webp', category: 'available', featured: false, sortOrder: 20 },
|
||||
{ id: 'pacha', title: 'Pacha', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/pacha.webp', category: 'available', featured: false, sortOrder: 21 },
|
||||
{ id: 'pacha-detalle', title: 'Pacha (Detalle)', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/pacha-detalle.webp', category: 'available', featured: false, sortOrder: 22 },
|
||||
{ id: 'warmy-arkanum', title: 'Warmy Arkanum', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/warmy-arkanum.webp', category: 'available', featured: false, sortOrder: 23 },
|
||||
{ id: 'sacro', title: 'Sacro', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/sacro.webp', category: 'available', featured: false, sortOrder: 24 },
|
||||
{ id: 'inti-om', title: 'Inti Om', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/inti-om.webp', category: 'available', featured: false, sortOrder: 25 },
|
||||
{ id: 'totem-astral', title: 'Totem Astral', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/totem-astral.webp', category: 'available', featured: false, sortOrder: 26 },
|
||||
{ id: 'sabiduria-ancestral', title: 'Sabidur\u00eda Ancestral', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/sabiduria-ancestral.webp', category: 'available', featured: false, sortOrder: 27 },
|
||||
{ id: 'amor-astral', title: 'Amor Astral', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/amor-astral.webp', category: 'available', featured: false, sortOrder: 28 },
|
||||
{ id: 'escencia-mistica', title: 'Escencia M\u00edstica', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/escencia-mistica.webp', category: 'available', featured: false, sortOrder: 29 },
|
||||
{ id: 'raiz', title: 'Ra\u00edz', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/raiz.webp', category: 'available', featured: false, sortOrder: 30 },
|
||||
{ id: 'uni-verso', title: 'Uni Verso', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/uni-verso.webp', category: 'available', featured: false, sortOrder: 31 },
|
||||
{ id: 'wayra', title: 'Wayra', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/wayra.webp', category: 'available', featured: false, sortOrder: 32 },
|
||||
{ id: 'la-illimani', title: 'La Illimani', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/art/la-illimani.webp', category: 'available', featured: false, sortOrder: 33 },
|
||||
{ id: 'eagle-jaguar', title: 'Eagle & Jaguar', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/about/process-1.webp', category: 'available', featured: false, sortOrder: 34 },
|
||||
{ id: 'kundalini', title: 'Kundalini', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/about/process-2.webp', category: 'available', featured: false, sortOrder: 35 },
|
||||
{ id: 'ancestral-totem', title: 'Ancestral Totem', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/about/process-3.webp', category: 'available', featured: false, sortOrder: 36 },
|
||||
{ id: 'detail-fire', title: 'Detail \u2014 Fire', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/about/process-5.webp', category: 'available', featured: false, sortOrder: 37 },
|
||||
{ id: 'sacred-union', title: 'Sacred Union', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/about/process-6.webp', category: 'available', featured: false, sortOrder: 38 },
|
||||
{ id: 'shiva-ii', title: 'Shiva II', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/about/process-7.webp', category: 'available', featured: false, sortOrder: 39 },
|
||||
{ id: 'golden-meditation', title: 'Golden Meditation', medium: 'Acrylic on canvas', dimensions: '', year: '', price: '', image: '/images/about/portrait-artist-2.webp', category: 'available', featured: false, sortOrder: 40 },
|
||||
];
|
||||
|
||||
export const seedEvents: ArtEvent[] = [
|
||||
{
|
||||
id: 'tribal-nights',
|
||||
title: 'Tribal Nights',
|
||||
description: 'A signature event weaving ritual, dance and immersive art into one transformative experience.',
|
||||
image: '/images/reevolution/tribal-night.jpg',
|
||||
date: '',
|
||||
link: '',
|
||||
section: 'reevolution',
|
||||
sortOrder: 1,
|
||||
},
|
||||
{
|
||||
id: 'tribal-experience',
|
||||
title: 'TRIBAL Experience',
|
||||
description: 'Group immersion (5\u20136 hours) exploring the unconscious through shamanic journeying, art alchemy and embodied movement.',
|
||||
image: '/images/reevolution/event-2.jpg',
|
||||
date: '',
|
||||
link: '',
|
||||
section: 'reevolution',
|
||||
sortOrder: 2,
|
||||
},
|
||||
{
|
||||
id: 'visionary-art-week',
|
||||
title: 'Visionary Art Week Z\u00fcrich',
|
||||
description: 'Curated week featuring international artists, performances, live music, talks and workshops.',
|
||||
image: '/images/reevolution/event-3.jpg',
|
||||
date: '',
|
||||
link: '',
|
||||
section: 'reevolution',
|
||||
sortOrder: 3,
|
||||
},
|
||||
{
|
||||
id: 'pulsar',
|
||||
title: 'PULSAR',
|
||||
description: 'Experimental experiences weaving DJ sets with expanded states of presence.',
|
||||
image: '/images/reevolution/dj-xhiva.jpg',
|
||||
date: '',
|
||||
link: '',
|
||||
section: 'reevolution',
|
||||
objectPosition: 'center top',
|
||||
sortOrder: 4,
|
||||
},
|
||||
];
|
||||
|
||||
export const seedServices: Service[] = [
|
||||
{
|
||||
id: 'healing-circle',
|
||||
title: 'Healing Circle',
|
||||
subtitle: 'El Camino del Jaguar',
|
||||
duration: 'Bi-weekly at Photobastei, Z\u00fcrich',
|
||||
description: 'Guided group healing circle combining crystal work, somatic practice and ritual in an intimate setting.',
|
||||
howItWorks: [
|
||||
'Bi-weekly gatherings at Photobastei cultural centre',
|
||||
'Crystal healing, somatic awareness and guided meditation',
|
||||
'Small group format for deeper connection',
|
||||
],
|
||||
whoItIsFor: [
|
||||
'Those seeking regular grounding practice',
|
||||
'People exploring healing in a supportive group',
|
||||
'Anyone drawn to crystal and somatic work',
|
||||
],
|
||||
image: '/images/services/crystal-therapy.webp',
|
||||
color: 'lavender',
|
||||
calendlyLink: '',
|
||||
highlighted: false,
|
||||
sortOrder: 1,
|
||||
},
|
||||
{
|
||||
id: 'tribal-experience',
|
||||
title: 'TRIBAL Experience',
|
||||
subtitle: 'Group Immersion',
|
||||
duration: '5\u20136 Hours',
|
||||
description: 'Deep group immersion weaving shamanic journeying, visionary art creation and embodied movement into a single transformative arc.',
|
||||
howItWorks: [
|
||||
'Extended 5\u20136 hour immersive group session',
|
||||
'Combines shamanic journeying, art alchemy and movement',
|
||||
'Guided by Ximena with live music and ritual elements',
|
||||
],
|
||||
whoItIsFor: [
|
||||
'Those ready for deep group transformation work',
|
||||
'Artists and creatives seeking embodied inspiration',
|
||||
'Anyone drawn to shamanic and ceremonial experiences',
|
||||
],
|
||||
image: '/images/reevolution/event-2.jpg',
|
||||
color: 'pink',
|
||||
calendlyLink: '',
|
||||
highlighted: false,
|
||||
sortOrder: 2,
|
||||
},
|
||||
{
|
||||
id: 'temazcal',
|
||||
title: 'Temazcal Ceremonies',
|
||||
subtitle: 'Medicine of the Four Elements',
|
||||
duration: 'Fire \u2013 Earth \u2013 Water \u2013 Air',
|
||||
description: 'Sweatlodge ceremonies guided for 15+ years, rooted in Native American ancestral tradition. Working with four elements within a contained ritual space supporting purification, closure and renewal.',
|
||||
howItWorks: [
|
||||
'Traditional sweatlodge ceremony with four elements',
|
||||
'Guided by Ximena with 15+ years of experience',
|
||||
'Held in natural settings with proper ritual preparation',
|
||||
],
|
||||
whoItIsFor: [
|
||||
'Those seeking purification and renewal',
|
||||
'People in life transitions or seeking closure',
|
||||
'Anyone drawn to earth-based ceremonial practice',
|
||||
],
|
||||
image: '/images/services/temazcal.jpg',
|
||||
color: 'mint',
|
||||
calendlyLink: '',
|
||||
highlighted: false,
|
||||
sortOrder: 3,
|
||||
},
|
||||
{
|
||||
id: 'retreats',
|
||||
title: 'Retreats',
|
||||
subtitle: 'Immersive Multi-Day Experiences',
|
||||
duration: 'Weekend & Multi-Day',
|
||||
description: 'Multi-day immersions combining art, ceremony, nature and inner work in carefully curated settings.',
|
||||
howItWorks: [
|
||||
'Multi-day immersive retreat in nature settings',
|
||||
'Combines art creation, ceremony and contemplative practice',
|
||||
'Small group format with personal attention',
|
||||
],
|
||||
whoItIsFor: [
|
||||
'Those ready for deep, sustained inner work',
|
||||
'People seeking a break from daily life for transformation',
|
||||
'Anyone wanting to combine art and spiritual practice',
|
||||
],
|
||||
image: '/images/services/deep-integration.webp',
|
||||
color: 'rose',
|
||||
calendlyLink: '',
|
||||
highlighted: false,
|
||||
sortOrder: 4,
|
||||
},
|
||||
{
|
||||
id: 'painting-collection',
|
||||
title: 'Painting & Art Collection',
|
||||
subtitle: 'Original Visionary Artworks',
|
||||
duration: 'Commission & Collection',
|
||||
description: 'Original paintings, commissions and art collection. Each work emerges from ritual process \u2014 portals for transformation and contemplation.',
|
||||
howItWorks: [
|
||||
'Browse available works in the gallery',
|
||||
'Commission a custom piece with personal consultation',
|
||||
'Worldwide shipping with certificate of authenticity',
|
||||
],
|
||||
whoItIsFor: [
|
||||
'Art collectors drawn to visionary and spiritual art',
|
||||
'Those wanting a meaningful artwork for their space',
|
||||
'Anyone seeking a personal commission',
|
||||
],
|
||||
image: '/images/art/featured.jpg?v=2',
|
||||
color: 'pink',
|
||||
calendlyLink: '',
|
||||
highlighted: true,
|
||||
sortOrder: 5,
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
export interface Artwork {
|
||||
id: string;
|
||||
title: string;
|
||||
medium: string;
|
||||
dimensions: string;
|
||||
year: string;
|
||||
price: string;
|
||||
image: string;
|
||||
category: 'available' | 'sold' | 'private';
|
||||
featured: boolean;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
export interface ArtEvent {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
date: string;
|
||||
link: string;
|
||||
section: 'xhiva' | 'reevolution';
|
||||
objectPosition?: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
export interface Service {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
duration: string;
|
||||
description: string;
|
||||
howItWorks: string[];
|
||||
whoItIsFor: string[];
|
||||
image: string;
|
||||
color: string;
|
||||
calendlyLink: string;
|
||||
highlighted: boolean;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getTokenFromCookies, verifySessionToken } from './lib/auth';
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl;
|
||||
|
||||
// Protect /admin/* and /api/admin/* (except login)
|
||||
const isAdminPage = pathname.startsWith('/admin') && pathname !== '/admin/login';
|
||||
const isAdminApi = pathname.startsWith('/api/admin') && pathname !== '/api/admin/login';
|
||||
|
||||
if (isAdminPage || isAdminApi) {
|
||||
const cookieHeader = request.headers.get('cookie');
|
||||
const token = getTokenFromCookies(cookieHeader);
|
||||
|
||||
if (!token || !(await verifySessionToken(token))) {
|
||||
if (isAdminApi) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
return NextResponse.redirect(new URL('/admin/login', request.url));
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/admin/:path*', '/api/admin/:path*'],
|
||||
};
|
||||
Loading…
Reference in New Issue