Add untracked source files from site restructure
CI/CD / deploy (push) Successful in 1m18s
Details
CI/CD / deploy (push) Successful in 1m18s
Details
Components, CMS, data modules, and page routes were created during the Feb 23 restructure but never committed to git. This broke CI builds as git clone wouldn't include them. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c7b97aa0cc
commit
991059020b
|
|
@ -0,0 +1,24 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { readData, writeData, validateAuth } from '@/lib/cms';
|
||||
import { featuredArtworks, allArtworks } from '@/lib/data/artworks';
|
||||
|
||||
export async function GET() {
|
||||
const featured = readData('artworks-featured', featuredArtworks);
|
||||
const all = readData('artworks', allArtworks);
|
||||
return NextResponse.json({ featured, all });
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (!validateAuth(request)) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { featured, all } = await request.json();
|
||||
if (featured) writeData('artworks-featured', featured);
|
||||
if (all) writeData('artworks', all);
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid data' }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { password } = await request.json();
|
||||
const expected = process.env.ADMIN_PASSWORD;
|
||||
|
||||
if (!expected) {
|
||||
return NextResponse.json({ error: 'Admin password not configured' }, { status: 500 });
|
||||
}
|
||||
|
||||
if (password !== expected) {
|
||||
return NextResponse.json({ error: 'Invalid password' }, { status: 401 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { readData, writeData, validateAuth } from '@/lib/cms';
|
||||
import { events, joinRoles } from '@/lib/data/events';
|
||||
|
||||
export async function GET() {
|
||||
const evts = readData('events', events);
|
||||
const roles = readData('join-roles', joinRoles);
|
||||
return NextResponse.json({ events: evts, joinRoles: roles });
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (!validateAuth(request)) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
if (body.events) writeData('events', body.events);
|
||||
if (body.joinRoles) writeData('join-roles', body.joinRoles);
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid data' }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { readData, writeData, validateAuth } from '@/lib/cms';
|
||||
import { services, methodologySteps } from '@/lib/data/services';
|
||||
|
||||
export async function GET() {
|
||||
const svc = readData('services', services);
|
||||
const steps = readData('methodology-steps', methodologySteps);
|
||||
return NextResponse.json({ services: svc, methodologySteps: steps });
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (!validateAuth(request)) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
if (body.services) writeData('services', body.services);
|
||||
if (body.methodologySteps) writeData('methodology-steps', body.methodologySteps);
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid data' }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { readData, writeData, validateAuth } from '@/lib/cms';
|
||||
import { testimonials } from '@/lib/data/testimonials';
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json(readData('testimonials', testimonials));
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (!validateAuth(request)) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await request.json();
|
||||
writeData('testimonials', data);
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid data' }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { writeFileSync, mkdirSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { validateAuth } from '@/lib/cms';
|
||||
|
||||
const UPLOAD_DIR = process.env.CMS_UPLOAD_DIR || '/app/data/uploads';
|
||||
|
||||
function sanitizeFilename(name: string): string {
|
||||
return name
|
||||
.replace(/[^a-zA-Z0-9._-]/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (!validateAuth(request)) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||
if (file.size > maxSize) {
|
||||
return NextResponse.json({ error: 'File too large (max 10MB)' }, { status: 400 });
|
||||
}
|
||||
|
||||
const allowed = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
|
||||
if (!allowed.includes(file.type)) {
|
||||
return NextResponse.json({ error: 'Invalid file type. Allowed: jpg, png, webp, gif' }, { status: 400 });
|
||||
}
|
||||
|
||||
mkdirSync(UPLOAD_DIR, { recursive: true });
|
||||
|
||||
const timestamp = Date.now();
|
||||
const filename = `${timestamp}-${sanitizeFilename(file.name)}`;
|
||||
const filePath = path.join(UPLOAD_DIR, filename);
|
||||
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
writeFileSync(filePath, buffer);
|
||||
|
||||
const url = `/api/uploads/${filename}`;
|
||||
return NextResponse.json({ url, filename });
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Upload failed' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
import type { Metadata } from 'next';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { featuredArtworks as defaultFeatured, allArtworks as defaultAll } from '@/lib/data/artworks';
|
||||
import { readData } from '@/lib/cms';
|
||||
import type { Artwork } from '@/lib/data/artworks';
|
||||
import LightboxGallery from '@/components/Lightbox';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Art | XHIVA ART - Visionary Artworks by Ximena Xaguar',
|
||||
description: 'Explore the visionary art of Ximena Xaguar — ritual paintings, sacred imagery and living portals born from ancestral memory, shadow work and transformation.',
|
||||
};
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default function ArtPage() {
|
||||
const featuredArtworks = readData<Artwork>('artworks-featured', defaultFeatured);
|
||||
const allArtworks = readData<Artwork>('artworks', defaultAll);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Page Hero */}
|
||||
<section className="page-hero relative flex flex-col items-center justify-center text-center px-6">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src="/images/art/featured.jpg?v=2"
|
||||
alt="Ritual Art Alchemy"
|
||||
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="relative z-10 max-w-4xl mx-auto">
|
||||
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
VISUAL ALCHEMY
|
||||
</p>
|
||||
<h1 className="text-5xl md:text-6xl font-light tracking-wider mb-6">
|
||||
Ritual Art Alchemy
|
||||
</h1>
|
||||
<div className="divider"></div>
|
||||
<p className="text-lg md:text-xl max-w-2xl mx-auto leading-relaxed 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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Artist Statement */}
|
||||
<section className="section bg-white">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<p className="text-lg leading-relaxed opacity-80 mb-6">
|
||||
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>
|
||||
<p className="text-lg leading-relaxed opacity-80">
|
||||
Trained in the tradition of Ernst Fuchs in Vienna, rooted in the ancestral art of Bolivia,
|
||||
and shaped by decades of ceremonial practice — each work carries the energy of its
|
||||
own birth, death and rebirth.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Featured Available Artworks */}
|
||||
<section 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">
|
||||
AVAILABLE WORKS
|
||||
</p>
|
||||
<h2 className="text-4xl md:text-5xl font-light mb-6">
|
||||
Featured Artworks
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{featuredArtworks.map((artwork, index) => (
|
||||
<div key={index} className="group">
|
||||
<div className="relative aspect-[3/4] rounded-xl overflow-hidden shadow-lg mb-4">
|
||||
<Image
|
||||
src={artwork.src}
|
||||
alt={artwork.title}
|
||||
fill
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="text-xl font-light mb-1">{artwork.title}</h3>
|
||||
{artwork.medium && (
|
||||
<p className="font-sans-alt text-xs tracking-widest text-[var(--text-muted)] mb-2">
|
||||
{artwork.medium}
|
||||
</p>
|
||||
)}
|
||||
<p className="price-tag">{artwork.price}</p>
|
||||
<Link href="/contact" className="inline-block mt-3 font-sans-alt text-xs tracking-widest text-[var(--accent-gold)] hover:text-[var(--text-dark)] transition-colors">
|
||||
Inquire →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Biography Brief */}
|
||||
<section className="section bg-white">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="grid md:grid-cols-2 gap-12 items-center">
|
||||
<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]"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
THE ARTIST
|
||||
</p>
|
||||
<h2 className="text-4xl font-light mb-6">
|
||||
Ximena Xaguar
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
<p className="text-lg leading-relaxed mb-6 opacity-80">
|
||||
Born in Bolivia, trained in the Visionary Realism tradition of Ernst Fuchs in Vienna.
|
||||
Living and working between Switzerland and South America since 1996.
|
||||
</p>
|
||||
<p className="text-lg leading-relaxed mb-8 opacity-80">
|
||||
Her paintings weave ancestral cosmovision with contemporary expression, carrying
|
||||
the energy of ceremony, transformation and the sacred feminine.
|
||||
</p>
|
||||
<Link href="/about" className="btn-outline">
|
||||
Full Biography
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Full Gallery */}
|
||||
<section 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>
|
||||
|
||||
<LightboxGallery artworks={allArtworks} />
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,278 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
const defaultServiceOptions = [
|
||||
'Crystal Therapy',
|
||||
'Temazcal',
|
||||
'Premium Transformational Session',
|
||||
'Soul Portrait — Art Alchemy',
|
||||
'Art Inquiry / Commission',
|
||||
'Re Evolution Art Collaboration',
|
||||
'Other',
|
||||
];
|
||||
|
||||
export default function ContactPage() {
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [service, setService] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [serviceOptions, setServiceOptions] = useState(defaultServiceOptions);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/cms/services')
|
||||
.then((res) => res.ok ? res.json() : null)
|
||||
.then((data) => {
|
||||
if (data?.services?.length) {
|
||||
const names = data.services.map((s: { title: string }) => s.title);
|
||||
setServiceOptions([...names, 'Art Inquiry / Commission', 'Re Evolution Art Collaboration', 'Other']);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!name.trim() || !email.trim() || !message.trim()) {
|
||||
setStatus('error');
|
||||
setErrorMessage('Please fill in all required 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(),
|
||||
service: service || undefined,
|
||||
message: message.trim(),
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || 'Something went wrong');
|
||||
}
|
||||
|
||||
setStatus('success');
|
||||
setName('');
|
||||
setEmail('');
|
||||
setService('');
|
||||
setMessage('');
|
||||
} catch (err) {
|
||||
setStatus('error');
|
||||
setErrorMessage(err instanceof Error ? err.message : 'Failed to send message. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Page Hero */}
|
||||
<section className="page-hero relative flex flex-col items-center justify-center text-center px-6">
|
||||
<div className="absolute inset-0 z-0 bg-gradient-to-b from-[var(--bg-cream)] via-white to-[var(--bg-cream)]" />
|
||||
<div className="relative z-10 max-w-4xl mx-auto">
|
||||
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
GET IN TOUCH
|
||||
</p>
|
||||
<h1 className="text-5xl md:text-6xl font-light tracking-wider mb-6">
|
||||
Begin Your Journey
|
||||
</h1>
|
||||
<div className="divider"></div>
|
||||
<p className="text-lg md:text-xl max-w-2xl mx-auto leading-relaxed opacity-80">
|
||||
For inquiries about sessions, art commissions, or event collaborations,
|
||||
please reach out through the form below or connect on social media.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Contact Form + Sidebar */}
|
||||
<section className="section bg-white">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="grid md:grid-cols-3 gap-12">
|
||||
{/* Form */}
|
||||
<div className="md:col-span-2">
|
||||
{status === 'success' ? (
|
||||
<div className="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="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="service">
|
||||
SERVICE
|
||||
</label>
|
||||
<select
|
||||
id="service"
|
||||
value={service}
|
||||
onChange={(e) => setService(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 bg-white"
|
||||
disabled={status === 'loading'}
|
||||
>
|
||||
<option value="">Select a service (optional)</option>
|
||||
{serviceOptions.map((opt) => (
|
||||
<option key={opt} value={opt}>{opt}</option>
|
||||
))}
|
||||
</select>
|
||||
</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={6}
|
||||
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>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="text-center md:text-left">
|
||||
<h3 className="text-2xl font-light mb-6">Connect</h3>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<p className="font-sans-alt text-xs tracking-widest text-[var(--accent-gold)] mb-2">
|
||||
INSTAGRAM
|
||||
</p>
|
||||
<a
|
||||
href="https://instagram.com/ximena_xaguar"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[var(--text-dark)] hover:text-[var(--accent-gold)] transition-colors"
|
||||
>
|
||||
@ximena_xaguar
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-sans-alt text-xs tracking-widest text-[var(--accent-gold)] mb-2">
|
||||
XHIVA ART
|
||||
</p>
|
||||
<a
|
||||
href="https://instagram.com/xhiva_art"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[var(--text-dark)] hover:text-[var(--accent-gold)] transition-colors"
|
||||
>
|
||||
@xhiva_art
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-sans-alt text-xs tracking-widest text-[var(--accent-gold)] mb-2">
|
||||
RE EVOLUTION ART
|
||||
</p>
|
||||
<a
|
||||
href="https://instagram.com/reevolutionart"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[var(--text-dark)] hover:text-[var(--accent-gold)] transition-colors"
|
||||
>
|
||||
@reevolutionart
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-sans-alt text-xs tracking-widest text-[var(--accent-gold)] mb-2">
|
||||
FACEBOOK
|
||||
</p>
|
||||
<a
|
||||
href="https://www.facebook.com/XimenaXhivart"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[var(--text-dark)] hover:text-[var(--accent-gold)] transition-colors"
|
||||
>
|
||||
Ximena Xhivart
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-sans-alt text-xs tracking-widest text-[var(--accent-gold)] mb-2">
|
||||
SOUNDCLOUD
|
||||
</p>
|
||||
<a
|
||||
href="https://soundcloud.com/xhiva"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[var(--text-dark)] hover:text-[var(--accent-gold)] transition-colors"
|
||||
>
|
||||
XHIVA
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-10">
|
||||
<Link href="/services" className="btn-outline text-sm">
|
||||
View Sessions & Pricing
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import type { Metadata } from 'next';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Our Ambassadors | Re Evolution Art',
|
||||
description: 'Re Evolution Art Ambassador Program — representing visionary culture across borders and communities.',
|
||||
};
|
||||
|
||||
export default function OurAmbassadorsPage() {
|
||||
return (
|
||||
<>
|
||||
{/* Page Hero */}
|
||||
<section className="page-hero relative flex flex-col items-center justify-center text-center px-6 dark-section">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src="/images/reevolution/event-3.jpg"
|
||||
alt="Our Ambassadors"
|
||||
fill
|
||||
className="object-cover opacity-30"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10 max-w-4xl mx-auto">
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
RE EVOLUTION ART
|
||||
</p>
|
||||
<h1 style={{ fontFamily: "'Krown', sans-serif", fontWeight: 700 }} className="text-5xl md:text-6xl mb-6">
|
||||
Our Ambassadors
|
||||
</h1>
|
||||
<div className="divider"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Program Description */}
|
||||
<section className="section dark-section">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
THE PROGRAM
|
||||
</p>
|
||||
<h2 style={{ fontFamily: "'Krown', sans-serif", fontWeight: 700 }} className="text-4xl font-light mb-6">
|
||||
Cultural Bridges
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-lg leading-relaxed mb-6 opacity-80">
|
||||
Our Ambassadors represent Re Evolution Art in their regions and communities, bridging
|
||||
cultures through art, dialogue and creative collaboration. They are the connective tissue
|
||||
of our platform — bringing the vision into new territories and expanding the reach of
|
||||
conscious cultural expression.
|
||||
</p>
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-lg leading-relaxed mb-12 opacity-80">
|
||||
If you feel called to represent visionary culture in your community, we want to hear from you.
|
||||
</p>
|
||||
|
||||
<div className="inline-block border border-[var(--accent-gold)] rounded-2xl p-12">
|
||||
<p style={{ fontFamily: "'Krown', sans-serif", fontWeight: 700 }} className="text-2xl text-[var(--accent-gold)] mb-4">
|
||||
Coming Soon
|
||||
</p>
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-sm opacity-60 mb-8">
|
||||
Ambassador profiles will be featured here as the program launches.
|
||||
</p>
|
||||
<Link href="/contact" className="btn-outline btn-outline-light" style={{ fontFamily: "'Narrenschiff', sans-serif" }}>
|
||||
Apply to Become an Ambassador
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Back link */}
|
||||
<section className="section darker-section text-center">
|
||||
<Link href="/re-evolution-art" className="btn-outline btn-outline-light" style={{ fontFamily: "'Narrenschiff', sans-serif" }}>
|
||||
Back to Re Evolution Art
|
||||
</Link>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import type { Metadata } from 'next';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Our Artisans | Re Evolution Art',
|
||||
description: 'Re Evolution Art Artisan Program — handcrafted goods, sacred objects and healing tools from independent creators.',
|
||||
};
|
||||
|
||||
export default function OurArtisansPage() {
|
||||
return (
|
||||
<>
|
||||
{/* Page Hero */}
|
||||
<section className="page-hero relative flex flex-col items-center justify-center text-center px-6 dark-section">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src="/images/reevolution/event-5.jpg"
|
||||
alt="Our Artisans"
|
||||
fill
|
||||
className="object-cover opacity-30"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10 max-w-4xl mx-auto">
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
RE EVOLUTION ART
|
||||
</p>
|
||||
<h1 style={{ fontFamily: "'Krown', sans-serif", fontWeight: 700 }} className="text-5xl md:text-6xl mb-6">
|
||||
Our Artisans
|
||||
</h1>
|
||||
<div className="divider"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Program Description */}
|
||||
<section className="section dark-section">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
HANDCRAFTED
|
||||
</p>
|
||||
<h2 style={{ fontFamily: "'Krown', sans-serif", fontWeight: 700 }} className="text-4xl font-light mb-6">
|
||||
Sacred Objects & Healing Tools
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-lg leading-relaxed mb-6 opacity-80">
|
||||
Our Artisans bring handcrafted goods, sacred objects, healing tools and ceremonial
|
||||
instruments to the Re Evolution Art community. Each piece carries intention, craft
|
||||
and connection to tradition.
|
||||
</p>
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-lg leading-relaxed mb-12 opacity-80">
|
||||
We welcome independent creators who work with natural materials, ancestral techniques
|
||||
and conscious intention. If your craft serves healing, beauty and cultural preservation,
|
||||
we want to collaborate.
|
||||
</p>
|
||||
|
||||
<div className="inline-block border border-[var(--accent-gold)] rounded-2xl p-12">
|
||||
<p style={{ fontFamily: "'Krown', sans-serif", fontWeight: 700 }} className="text-2xl text-[var(--accent-gold)] mb-4">
|
||||
Coming Soon
|
||||
</p>
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-sm opacity-60 mb-8">
|
||||
Artisan profiles and marketplace will be featured here as the program develops.
|
||||
</p>
|
||||
<Link href="/contact" className="btn-outline btn-outline-light" style={{ fontFamily: "'Narrenschiff', sans-serif" }}>
|
||||
Apply as an Artisan
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Back link */}
|
||||
<section className="section darker-section text-center">
|
||||
<Link href="/re-evolution-art" className="btn-outline btn-outline-light" style={{ fontFamily: "'Narrenschiff', sans-serif" }}>
|
||||
Back to Re Evolution Art
|
||||
</Link>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
import type { Metadata } from 'next';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Our Artists | Re Evolution Art',
|
||||
description: 'Meet the artists of Re Evolution Art — Erofex and XHIVA. Visionary creators bridging ancestral tradition with contemporary expression.',
|
||||
};
|
||||
|
||||
export default function OurArtistsPage() {
|
||||
return (
|
||||
<>
|
||||
{/* Page Hero */}
|
||||
<section className="page-hero relative flex flex-col items-center justify-center text-center px-6 dark-section">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src="/images/reevolution/event-2.jpg"
|
||||
alt="Our Artists"
|
||||
fill
|
||||
className="object-cover opacity-30"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10 max-w-4xl mx-auto">
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
RE EVOLUTION ART
|
||||
</p>
|
||||
<h1 style={{ fontFamily: "'Krown', sans-serif", fontWeight: 700 }} className="text-5xl md:text-6xl mb-6">
|
||||
Our Artists
|
||||
</h1>
|
||||
<div className="divider"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Erofex */}
|
||||
<section className="section dark-section">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="grid md:grid-cols-2 gap-12 items-center">
|
||||
<div className="relative aspect-square rounded-2xl overflow-hidden shadow-xl">
|
||||
<Image
|
||||
src="/images/reevolution/event-4.jpg"
|
||||
alt="Erofex"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
ARTIST / ENGINEER
|
||||
</p>
|
||||
<h2 style={{ fontFamily: "'Krown', sans-serif", fontWeight: 700 }} className="text-4xl mb-6">
|
||||
Erofex
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-lg leading-relaxed mb-6 opacity-80">
|
||||
Bolivian artist and sound engineer, Erofex has been a pioneer in the techno and psytrance
|
||||
scene since 1997. As co-founder of Neurotrance and Audioreaktor, he brings decades of
|
||||
experience in electronic music production, live performance and sound design.
|
||||
</p>
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-lg leading-relaxed mb-8 opacity-80">
|
||||
His work bridges the technical precision of sound engineering with the raw energy of
|
||||
underground electronic culture, creating immersive sonic landscapes that drive the
|
||||
Re Evolution Art experience.
|
||||
</p>
|
||||
<a
|
||||
href="https://soundcloud.com/erofex"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-outline btn-outline-light"
|
||||
style={{ fontFamily: "'Narrenschiff', sans-serif" }}
|
||||
>
|
||||
SoundCloud
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* XHIVA */}
|
||||
<section className="section darker-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 style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
DJ / VISUAL ARTIST
|
||||
</p>
|
||||
<h2 style={{ fontFamily: "'Krown', sans-serif", fontWeight: 700 }} className="text-4xl mb-6">
|
||||
XHIVA
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-lg leading-relaxed mb-6 opacity-80">
|
||||
DJing since 2001, XHIVA weaves shamanic, indie, afro, organic, melodic and house
|
||||
music into immersive sonic journeys. Her sets are ceremonial in nature — designed
|
||||
to move the body while opening the heart and expanding consciousness.
|
||||
</p>
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-lg leading-relaxed mb-8 opacity-80">
|
||||
As a multidisciplinary visionary artist, XHIVA brings the same devotion to her DJ
|
||||
practice as to her paintings and ceremonies — each set is a ritual, each transition
|
||||
an invitation to go deeper.
|
||||
</p>
|
||||
<a
|
||||
href="https://soundcloud.com/xhiva"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-outline btn-outline-light"
|
||||
style={{ fontFamily: "'Narrenschiff', sans-serif" }}
|
||||
>
|
||||
SoundCloud
|
||||
</a>
|
||||
</div>
|
||||
<div className="relative aspect-square rounded-2xl overflow-hidden shadow-xl order-1 md:order-2">
|
||||
<Image
|
||||
src="/images/reevolution/dj-xhiva.jpg"
|
||||
alt="DJ XHIVA"
|
||||
fill
|
||||
className="object-cover object-top"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Back to Re Evolution Art */}
|
||||
<section className="section dark-section text-center">
|
||||
<Link href="/re-evolution-art" className="btn-outline btn-outline-light" style={{ fontFamily: "'Narrenschiff', sans-serif" }}>
|
||||
Back to Re Evolution Art
|
||||
</Link>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
import type { Metadata } from 'next';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { events as defaultEvents, joinRoles as defaultRoles } from '@/lib/data/events';
|
||||
import { readData } from '@/lib/cms';
|
||||
import type { ReEvolutionEvent } from '@/lib/data/events';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Re Evolution Art | Cultural Platform - Bridging Cultures Through Art',
|
||||
description: 'A visionary cultural platform based in Switzerland and rooted in Bolivia. Exhibitions, TRIBAL events, immersive gatherings and collaborative projects.',
|
||||
};
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default function ReEvolutionArtPage() {
|
||||
const events = readData<ReEvolutionEvent>('events', defaultEvents);
|
||||
const joinRoles = readData<{ title: string; description: string }>('join-roles', defaultRoles);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Hero */}
|
||||
<section className="page-hero relative flex flex-col items-center justify-center text-center px-6 dark-section">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src="/images/reevolution/event-1.jpg"
|
||||
alt="Re Evolution Art"
|
||||
fill
|
||||
className="object-cover opacity-30"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10 max-w-4xl mx-auto">
|
||||
<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.3em] text-[var(--accent-gold)] mb-4">
|
||||
BRIDGING CULTURES THROUGH ART
|
||||
</p>
|
||||
<h1 style={{ fontFamily: "'Krown', sans-serif", fontWeight: 700 }} className="text-5xl md:text-6xl mb-6">
|
||||
Re Evolution Art
|
||||
</h1>
|
||||
<div className="divider"></div>
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-lg md:text-xl max-w-2xl mx-auto leading-relaxed opacity-80">
|
||||
Cultural Hub for Visionaries
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Who We Are */}
|
||||
<section className="section dark-section">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
WHO WE ARE
|
||||
</p>
|
||||
<h2 style={{ fontFamily: "'Krown', sans-serif", fontWeight: 700 }} className="text-4xl font-light mb-6">
|
||||
A Visionary Cultural Platform
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-lg leading-relaxed mb-6 opacity-80">
|
||||
Re Evolution Art is 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>
|
||||
</section>
|
||||
|
||||
{/* Our Core Vision */}
|
||||
<section className="section darker-section">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
OUR CORE VISION
|
||||
</p>
|
||||
<h2 style={{ fontFamily: "'Krown', sans-serif", fontWeight: 700 }} className="text-4xl font-light mb-6">
|
||||
Art as Transformation
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-lg leading-relaxed mb-6 opacity-80">
|
||||
We believe art is not decoration — it is a living force. A bridge between the inner and outer
|
||||
worlds, between ancient wisdom and contemporary creation. Our events and projects are designed
|
||||
to create spaces where this force can move freely — through painting, music, dance, ritual
|
||||
and dialogue.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Our Roots */}
|
||||
<section className="section dark-section">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="grid md:grid-cols-2 gap-12 items-center">
|
||||
<div className="relative aspect-[4/3] rounded-2xl overflow-hidden shadow-xl">
|
||||
<Image
|
||||
src="/images/reevolution/event-3.jpg"
|
||||
alt="Re Evolution Art Event"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
OUR ROOTS
|
||||
</p>
|
||||
<h2 style={{ fontFamily: "'Krown', sans-serif", fontWeight: 700 }} className="text-4xl font-light mb-6">
|
||||
Bolivia & Switzerland
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-lg leading-relaxed mb-6 opacity-80">
|
||||
Founded by Ximena Xaguar, Re Evolution Art carries the DNA of both Andean cosmovision
|
||||
and European visionary tradition. From the ceremonial landscapes of Bolivia to the creative
|
||||
scenes of Zurich, we weave a bridge that honours both worlds.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Projects */}
|
||||
<section className="section darker-section">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
PROJECTS
|
||||
</p>
|
||||
<h2 style={{ fontFamily: "'Krown', sans-serif", fontWeight: 700 }} className="text-4xl md:text-5xl mb-6">
|
||||
Our Events & Initiatives
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
</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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Join Us */}
|
||||
<section className="section dark-section">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
GET INVOLVED
|
||||
</p>
|
||||
<h2 style={{ fontFamily: "'Krown', sans-serif", fontWeight: 700 }} className="text-4xl md:text-5xl mb-6">
|
||||
Join Us
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-lg max-w-2xl mx-auto opacity-80">
|
||||
Re Evolution Art is a growing community. There are many ways to participate.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{joinRoles.map((role, index) => (
|
||||
<div key={index} className="role-card">
|
||||
<h3 style={{ fontFamily: "'Krown', sans-serif", fontWeight: 700 }} className="text-lg mb-2 text-[var(--accent-gold)]">
|
||||
{role.title}
|
||||
</h3>
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-sm opacity-70 leading-relaxed">
|
||||
{role.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mt-12">
|
||||
<Link href="/contact" className="btn-outline btn-outline-light" style={{ fontFamily: "'Narrenschiff', sans-serif" }}>
|
||||
Get in Touch
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Social Links */}
|
||||
<section className="section darker-section">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
FOLLOW US
|
||||
</p>
|
||||
<h2 style={{ fontFamily: "'Krown', sans-serif", fontWeight: 700 }} className="text-4xl font-light mb-6">
|
||||
Stay Connected
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
<div className="flex justify-center gap-6 mt-8">
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
import type { Metadata } from 'next';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { services as defaultServices, methodologySteps as defaultSteps } from '@/lib/data/services';
|
||||
import { readData } from '@/lib/cms';
|
||||
import type { Service } from '@/lib/data/services';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Services | XHIVA ART - One-to-One Sessions',
|
||||
description: 'Crystal Therapy, Temazcal ceremonies, Premium Transformational Sessions and Soul Portrait Art Alchemy. Containers of clarity, embodiment and transformation.',
|
||||
};
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default function ServicesPage() {
|
||||
const services = readData<Service>('services', defaultServices);
|
||||
const methodologySteps = readData<{ step: number; title: string; description: string }>('methodology-steps', defaultSteps);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Page Hero */}
|
||||
<section className="page-hero relative flex flex-col items-center justify-center text-center px-6">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src="/images/services/crystal-therapy.webp"
|
||||
alt="Sessions"
|
||||
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="relative z-10 max-w-4xl mx-auto">
|
||||
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
OFFERINGS
|
||||
</p>
|
||||
<h1 className="text-5xl md:text-6xl font-light tracking-wider mb-6">
|
||||
One-to-One Sessions
|
||||
</h1>
|
||||
<div className="divider"></div>
|
||||
<p className="text-lg md:text-xl max-w-2xl mx-auto leading-relaxed opacity-80">
|
||||
Containers of clarity, embodiment and transformation.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Service Cards */}
|
||||
{services.map((service, index) => (
|
||||
<section key={index} className={`section ${index % 2 === 0 ? 'bg-white' : ''}`}>
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className={`grid md:grid-cols-2 gap-12 items-center ${index % 2 === 1 ? 'md:[&>*:first-child]:order-2' : ''}`}>
|
||||
<div className="relative aspect-[4/3] rounded-2xl overflow-hidden shadow-xl">
|
||||
<Image
|
||||
src={service.image}
|
||||
alt={service.title}
|
||||
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">
|
||||
{service.subtitle.toUpperCase()}
|
||||
</p>
|
||||
<h2 className="text-4xl font-light mb-4">
|
||||
{service.title}
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
{service.price && (
|
||||
<p className="price-tag mb-4">{service.price}</p>
|
||||
)}
|
||||
<p className="font-sans-alt text-xs tracking-widest text-[var(--text-muted)] mb-6">
|
||||
{service.duration}
|
||||
</p>
|
||||
<p className="text-lg leading-relaxed mb-8 opacity-80">
|
||||
{service.longDescription || service.description}
|
||||
</p>
|
||||
|
||||
{service.recommendedFor && (
|
||||
<div className="text-left max-w-md mx-auto mb-8">
|
||||
<p className="font-sans-alt text-xs tracking-widest text-[var(--accent-gold)] mb-3">
|
||||
RECOMMENDED FOR
|
||||
</p>
|
||||
<ul className="space-y-2">
|
||||
{service.recommendedFor.map((item, i) => (
|
||||
<li key={i} className="text-sm leading-relaxed opacity-80 flex items-start gap-2">
|
||||
<span className="text-[var(--accent-gold)] mt-1">•</span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Link href="/contact" className="btn-outline">
|
||||
Book This Session
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
|
||||
{/* How Sessions Work */}
|
||||
<section className="section dark-section">
|
||||
<div className="max-w-4xl 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-4xl font-light mb-6">
|
||||
How Sessions Work
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{methodologySteps.map((step) => (
|
||||
<div key={step.step} className="methodology-step">
|
||||
<div className="flex items-center gap-4 mb-3">
|
||||
<span className="font-sans-alt text-2xl font-light text-[var(--accent-gold)]">
|
||||
{String(step.step).padStart(2, '0')}
|
||||
</span>
|
||||
<h3 className="text-xl font-light">{step.title}</h3>
|
||||
</div>
|
||||
<p className="text-sm leading-relaxed opacity-70 pl-12">
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Who These Sessions Are For */}
|
||||
<section className="section bg-white">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||
IS THIS FOR YOU?
|
||||
</p>
|
||||
<h2 className="text-4xl font-light mb-6">
|
||||
Who These Sessions Are For
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
<div className="grid md:grid-cols-2 gap-8 text-left mt-8">
|
||||
{[
|
||||
'Those navigating life transitions and seeking clarity',
|
||||
'People ready to integrate deep emotional or spiritual experiences',
|
||||
'Anyone drawn to somatic, energetic or ceremonial healing',
|
||||
'Creatives seeking to reconnect with their authentic expression',
|
||||
'Those processing grief, loss or significant change',
|
||||
'Individuals on a conscious path of self-discovery',
|
||||
].map((item, index) => (
|
||||
<div key={index} className="flex items-start gap-3">
|
||||
<span className="text-[var(--accent-gold)] mt-1">✓</span>
|
||||
<p className="text-lg leading-relaxed opacity-80">{item}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<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 text-center">
|
||||
<h2 className="text-4xl md:text-5xl font-light mb-6 text-[var(--text-light)]">
|
||||
Book a Session
|
||||
</h2>
|
||||
<div className="divider"></div>
|
||||
<p className="text-lg leading-relaxed mb-12 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. Reach out to begin.
|
||||
</p>
|
||||
<Link href="/contact" className="btn-filled" style={{ background: 'var(--accent-gold)', borderColor: 'var(--accent-gold)' }}>
|
||||
Get in Touch
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,715 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
interface Artwork {
|
||||
src: string;
|
||||
title: string;
|
||||
price?: string;
|
||||
available?: boolean;
|
||||
medium?: string;
|
||||
dimensions?: string;
|
||||
}
|
||||
|
||||
interface Service {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
duration: string;
|
||||
price?: string;
|
||||
description: string;
|
||||
longDescription?: string;
|
||||
color: string;
|
||||
image: string;
|
||||
highlighted?: boolean;
|
||||
recommendedFor?: string[];
|
||||
}
|
||||
|
||||
interface Testimonial {
|
||||
quote: string;
|
||||
author: string;
|
||||
context?: string;
|
||||
}
|
||||
|
||||
interface ReEvolutionEvent {
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
objectPosition?: string;
|
||||
}
|
||||
|
||||
type Tab = 'artworks' | 'services' | 'testimonials' | 'events';
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function getPassword(): string {
|
||||
return sessionStorage.getItem('admin_password') || '';
|
||||
}
|
||||
|
||||
async function apiFetch(url: string, options?: RequestInit) {
|
||||
const headers: Record<string, string> = {
|
||||
'X-Admin-Password': getPassword(),
|
||||
...(options?.headers as Record<string, string> || {}),
|
||||
};
|
||||
if (!(options?.body instanceof FormData)) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
return fetch(url, { ...options, headers });
|
||||
}
|
||||
|
||||
// ─── Auth Gate ───────────────────────────────────────────────────────────────
|
||||
|
||||
function AuthGate({ onAuth }: { onAuth: () => void }) {
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
const res = await fetch('/api/cms/auth', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password }),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
sessionStorage.setItem('admin_password', password);
|
||||
onAuth();
|
||||
} else {
|
||||
setError('Invalid password');
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-[var(--bg-cream)]">
|
||||
<form onSubmit={handleSubmit} className="bg-white p-8 rounded-xl shadow-lg max-w-sm w-full">
|
||||
<h1 className="text-2xl font-light mb-6 text-center">Site Admin</h1>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Password"
|
||||
className="w-full px-4 py-3 border border-gray-200 rounded-lg mb-4 focus:outline-none focus:border-[var(--accent-gold)]"
|
||||
autoFocus
|
||||
/>
|
||||
{error && <p className="text-red-600 text-sm mb-4">{error}</p>}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="btn-filled w-full"
|
||||
style={loading ? { opacity: 0.7, cursor: 'not-allowed' } : undefined}
|
||||
>
|
||||
{loading ? 'Checking...' : 'Sign In'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Image Upload Button ─────────────────────────────────────────────────────
|
||||
|
||||
function ImageUpload({ onUploaded }: { onUploaded: (url: string) => void }) {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
const handleFile = async (file: File) => {
|
||||
setUploading(true);
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
const res = await apiFetch('/api/cms/upload', { method: 'POST', body: form });
|
||||
if (res.ok) {
|
||||
const { url } = await res.json();
|
||||
onUploaded(url);
|
||||
} else {
|
||||
alert('Upload failed');
|
||||
}
|
||||
setUploading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<label className="inline-flex items-center gap-2 px-3 py-1.5 border border-gray-300 rounded-lg cursor-pointer hover:border-[var(--accent-gold)] transition-colors text-sm">
|
||||
{uploading ? 'Uploading...' : 'Upload Image'}
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={(e) => e.target.files?.[0] && handleFile(e.target.files[0])}
|
||||
disabled={uploading}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Artworks Tab ────────────────────────────────────────────────────────────
|
||||
|
||||
function ArtworksTab() {
|
||||
const [featured, setFeatured] = useState<Artwork[]>([]);
|
||||
const [all, setAll] = useState<Artwork[]>([]);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [editMode, setEditMode] = useState<'featured' | 'gallery'>('featured');
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const res = await apiFetch('/api/cms/artworks');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setFeatured(data.featured);
|
||||
setAll(data.all);
|
||||
setLoaded(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const save = async () => {
|
||||
setSaving(true);
|
||||
await apiFetch('/api/cms/artworks', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ featured, all }),
|
||||
});
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
const items = editMode === 'featured' ? featured : all;
|
||||
const setItems = editMode === 'featured' ? setFeatured : setAll;
|
||||
|
||||
const addItem = () => {
|
||||
const newItem: Artwork = { src: '', title: '' };
|
||||
if (editMode === 'featured') {
|
||||
newItem.price = '';
|
||||
newItem.medium = '';
|
||||
newItem.available = true;
|
||||
}
|
||||
setItems([...items, newItem]);
|
||||
};
|
||||
|
||||
const updateItem = (index: number, field: keyof Artwork, value: string | boolean) => {
|
||||
const updated = [...items];
|
||||
updated[index] = { ...updated[index], [field]: value };
|
||||
setItems(updated);
|
||||
};
|
||||
|
||||
const removeItem = (index: number) => {
|
||||
if (!confirm('Delete this artwork?')) return;
|
||||
setItems(items.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const moveItem = (index: number, direction: -1 | 1) => {
|
||||
const newIndex = index + direction;
|
||||
if (newIndex < 0 || newIndex >= items.length) return;
|
||||
const updated = [...items];
|
||||
[updated[index], updated[newIndex]] = [updated[newIndex], updated[index]];
|
||||
setItems(updated);
|
||||
};
|
||||
|
||||
if (!loaded) return <p className="p-8 text-center opacity-60">Loading...</p>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<button
|
||||
onClick={() => setEditMode('featured')}
|
||||
className={`px-4 py-2 rounded-lg text-sm ${editMode === 'featured' ? 'bg-[var(--accent-gold)] text-white' : 'border border-gray-300'}`}
|
||||
>
|
||||
Featured ({featured.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditMode('gallery')}
|
||||
className={`px-4 py-2 rounded-lg text-sm ${editMode === 'gallery' ? 'bg-[var(--accent-gold)] text-white' : 'border border-gray-300'}`}
|
||||
>
|
||||
Gallery ({all.length})
|
||||
</button>
|
||||
<div className="flex-1" />
|
||||
<button onClick={addItem} className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm">
|
||||
+ Add
|
||||
</button>
|
||||
<button onClick={save} disabled={saving} className="px-4 py-2 bg-[var(--accent-gold)] text-white rounded-lg text-sm">
|
||||
{saving ? 'Saving...' : 'Save All'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{items.map((item, i) => (
|
||||
<div key={i} className="border border-gray-200 rounded-lg p-4 bg-white">
|
||||
<div className="flex items-start gap-4">
|
||||
{item.src && (
|
||||
<div className="w-16 h-16 rounded overflow-hidden flex-shrink-0 bg-gray-100">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={item.src} alt={item.title} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<input
|
||||
value={item.title}
|
||||
onChange={(e) => updateItem(i, 'title', e.target.value)}
|
||||
placeholder="Title"
|
||||
className="px-3 py-2 border border-gray-200 rounded text-sm"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
value={item.src}
|
||||
onChange={(e) => updateItem(i, 'src', e.target.value)}
|
||||
placeholder="Image path (e.g. /images/art/...)"
|
||||
className="flex-1 px-3 py-2 border border-gray-200 rounded text-sm"
|
||||
/>
|
||||
<ImageUpload onUploaded={(url) => updateItem(i, 'src', url)} />
|
||||
</div>
|
||||
{editMode === 'featured' && (
|
||||
<>
|
||||
<input
|
||||
value={item.price || ''}
|
||||
onChange={(e) => updateItem(i, 'price', e.target.value)}
|
||||
placeholder="Price (e.g. CHF 4,800)"
|
||||
className="px-3 py-2 border border-gray-200 rounded text-sm"
|
||||
/>
|
||||
<input
|
||||
value={item.medium || ''}
|
||||
onChange={(e) => updateItem(i, 'medium', e.target.value)}
|
||||
placeholder="Medium"
|
||||
className="px-3 py-2 border border-gray-200 rounded text-sm"
|
||||
/>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.available ?? false}
|
||||
onChange={(e) => updateItem(i, 'available', e.target.checked)}
|
||||
/>
|
||||
Available
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<button onClick={() => moveItem(i, -1)} className="text-gray-400 hover:text-gray-600 text-xs" title="Move up">↑</button>
|
||||
<button onClick={() => moveItem(i, 1)} className="text-gray-400 hover:text-gray-600 text-xs" title="Move down">↓</button>
|
||||
<button onClick={() => removeItem(i)} className="text-red-400 hover:text-red-600 text-xs" title="Delete">×</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Services Tab ────────────────────────────────────────────────────────────
|
||||
|
||||
function ServicesTab() {
|
||||
const [items, setItems] = useState<Service[]>([]);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const res = await apiFetch('/api/cms/services');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setItems(data.services);
|
||||
setLoaded(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const save = async () => {
|
||||
setSaving(true);
|
||||
await apiFetch('/api/cms/services', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ services: items }),
|
||||
});
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
const updateItem = (index: number, field: keyof Service, value: string | boolean | string[]) => {
|
||||
const updated = [...items];
|
||||
updated[index] = { ...updated[index], [field]: value };
|
||||
setItems(updated);
|
||||
};
|
||||
|
||||
const addItem = () => {
|
||||
setItems([...items, {
|
||||
title: '', subtitle: '', duration: '', description: '',
|
||||
color: 'lavender', image: '',
|
||||
}]);
|
||||
};
|
||||
|
||||
const removeItem = (index: number) => {
|
||||
if (!confirm('Delete this service?')) return;
|
||||
setItems(items.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
if (!loaded) return <p className="p-8 text-center opacity-60">Loading...</p>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<span className="text-sm text-gray-500">{items.length} services</span>
|
||||
<div className="flex-1" />
|
||||
<button onClick={addItem} className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm">
|
||||
+ Add
|
||||
</button>
|
||||
<button onClick={save} disabled={saving} className="px-4 py-2 bg-[var(--accent-gold)] text-white rounded-lg text-sm">
|
||||
{saving ? 'Saving...' : 'Save All'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{items.map((item, i) => (
|
||||
<div key={i} className="border border-gray-200 rounded-lg p-4 bg-white">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="font-medium">{item.title || '(untitled)'}</h4>
|
||||
<button onClick={() => removeItem(i)} className="text-red-400 hover:text-red-600 text-sm">Delete</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<input
|
||||
value={item.title}
|
||||
onChange={(e) => updateItem(i, 'title', e.target.value)}
|
||||
placeholder="Title"
|
||||
className="px-3 py-2 border border-gray-200 rounded text-sm"
|
||||
/>
|
||||
<input
|
||||
value={item.subtitle}
|
||||
onChange={(e) => updateItem(i, 'subtitle', e.target.value)}
|
||||
placeholder="Subtitle"
|
||||
className="px-3 py-2 border border-gray-200 rounded text-sm"
|
||||
/>
|
||||
<input
|
||||
value={item.duration}
|
||||
onChange={(e) => updateItem(i, 'duration', e.target.value)}
|
||||
placeholder="Duration"
|
||||
className="px-3 py-2 border border-gray-200 rounded text-sm"
|
||||
/>
|
||||
<input
|
||||
value={item.price || ''}
|
||||
onChange={(e) => updateItem(i, 'price', e.target.value)}
|
||||
placeholder="Price (optional)"
|
||||
className="px-3 py-2 border border-gray-200 rounded text-sm"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
value={item.image}
|
||||
onChange={(e) => updateItem(i, 'image', e.target.value)}
|
||||
placeholder="Image path"
|
||||
className="flex-1 px-3 py-2 border border-gray-200 rounded text-sm"
|
||||
/>
|
||||
<ImageUpload onUploaded={(url) => updateItem(i, 'image', url)} />
|
||||
</div>
|
||||
<select
|
||||
value={item.color}
|
||||
onChange={(e) => updateItem(i, 'color', e.target.value)}
|
||||
className="px-3 py-2 border border-gray-200 rounded text-sm bg-white"
|
||||
>
|
||||
<option value="lavender">Lavender</option>
|
||||
<option value="mint">Mint</option>
|
||||
<option value="rose">Rose</option>
|
||||
<option value="pink">Pink</option>
|
||||
</select>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.highlighted ?? false}
|
||||
onChange={(e) => updateItem(i, 'highlighted', e.target.checked)}
|
||||
/>
|
||||
Highlighted
|
||||
</label>
|
||||
</div>
|
||||
<textarea
|
||||
value={item.description}
|
||||
onChange={(e) => updateItem(i, 'description', e.target.value)}
|
||||
placeholder="Short description"
|
||||
rows={2}
|
||||
className="w-full mt-3 px-3 py-2 border border-gray-200 rounded text-sm resize-none"
|
||||
/>
|
||||
<textarea
|
||||
value={item.longDescription || ''}
|
||||
onChange={(e) => updateItem(i, 'longDescription', e.target.value)}
|
||||
placeholder="Long description (optional)"
|
||||
rows={3}
|
||||
className="w-full mt-2 px-3 py-2 border border-gray-200 rounded text-sm resize-none"
|
||||
/>
|
||||
<div className="mt-2">
|
||||
<label className="block text-xs text-gray-500 mb-1">Recommended For (one per line)</label>
|
||||
<textarea
|
||||
value={(item.recommendedFor || []).join('\n')}
|
||||
onChange={(e) => updateItem(i, 'recommendedFor', e.target.value.split('\n').filter(Boolean))}
|
||||
rows={3}
|
||||
placeholder="One item per line"
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded text-sm resize-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Testimonials Tab ────────────────────────────────────────────────────────
|
||||
|
||||
function TestimonialsTab() {
|
||||
const [items, setItems] = useState<Testimonial[]>([]);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const res = await apiFetch('/api/cms/testimonials');
|
||||
if (res.ok) {
|
||||
setItems(await res.json());
|
||||
setLoaded(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const save = async () => {
|
||||
setSaving(true);
|
||||
await apiFetch('/api/cms/testimonials', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(items),
|
||||
});
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
const updateItem = (index: number, field: keyof Testimonial, value: string) => {
|
||||
const updated = [...items];
|
||||
updated[index] = { ...updated[index], [field]: value };
|
||||
setItems(updated);
|
||||
};
|
||||
|
||||
const addItem = () => {
|
||||
setItems([...items, { quote: '', author: '' }]);
|
||||
};
|
||||
|
||||
const removeItem = (index: number) => {
|
||||
if (!confirm('Delete this testimonial?')) return;
|
||||
setItems(items.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
if (!loaded) return <p className="p-8 text-center opacity-60">Loading...</p>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<span className="text-sm text-gray-500">{items.length} testimonials</span>
|
||||
<div className="flex-1" />
|
||||
<button onClick={addItem} className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm">
|
||||
+ Add
|
||||
</button>
|
||||
<button onClick={save} disabled={saving} className="px-4 py-2 bg-[var(--accent-gold)] text-white rounded-lg text-sm">
|
||||
{saving ? 'Saving...' : 'Save All'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{items.map((item, i) => (
|
||||
<div key={i} className="border border-gray-200 rounded-lg p-4 bg-white">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm font-medium">{item.author || '(no author)'}</span>
|
||||
<button onClick={() => removeItem(i)} className="text-red-400 hover:text-red-600 text-sm">Delete</button>
|
||||
</div>
|
||||
<textarea
|
||||
value={item.quote}
|
||||
onChange={(e) => updateItem(i, 'quote', e.target.value)}
|
||||
placeholder="Quote"
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded text-sm resize-none mb-2"
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<input
|
||||
value={item.author}
|
||||
onChange={(e) => updateItem(i, 'author', e.target.value)}
|
||||
placeholder="Author"
|
||||
className="px-3 py-2 border border-gray-200 rounded text-sm"
|
||||
/>
|
||||
<input
|
||||
value={item.context || ''}
|
||||
onChange={(e) => updateItem(i, 'context', e.target.value)}
|
||||
placeholder="Context (e.g. Crystal Therapy)"
|
||||
className="px-3 py-2 border border-gray-200 rounded text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Events Tab ──────────────────────────────────────────────────────────────
|
||||
|
||||
function EventsTab() {
|
||||
const [items, setItems] = useState<ReEvolutionEvent[]>([]);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const res = await apiFetch('/api/cms/events');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setItems(data.events);
|
||||
setLoaded(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const save = async () => {
|
||||
setSaving(true);
|
||||
await apiFetch('/api/cms/events', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ events: items }),
|
||||
});
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
const updateItem = (index: number, field: keyof ReEvolutionEvent, value: string) => {
|
||||
const updated = [...items];
|
||||
updated[index] = { ...updated[index], [field]: value };
|
||||
setItems(updated);
|
||||
};
|
||||
|
||||
const addItem = () => {
|
||||
setItems([...items, { title: '', description: '', image: '' }]);
|
||||
};
|
||||
|
||||
const removeItem = (index: number) => {
|
||||
if (!confirm('Delete this event?')) return;
|
||||
setItems(items.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
if (!loaded) return <p className="p-8 text-center opacity-60">Loading...</p>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<span className="text-sm text-gray-500">{items.length} events</span>
|
||||
<div className="flex-1" />
|
||||
<button onClick={addItem} className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm">
|
||||
+ Add
|
||||
</button>
|
||||
<button onClick={save} disabled={saving} className="px-4 py-2 bg-[var(--accent-gold)] text-white rounded-lg text-sm">
|
||||
{saving ? 'Saving...' : 'Save All'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{items.map((item, i) => (
|
||||
<div key={i} className="border border-gray-200 rounded-lg p-4 bg-white">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="font-medium">{item.title || '(untitled)'}</h4>
|
||||
<button onClick={() => removeItem(i)} className="text-red-400 hover:text-red-600 text-sm">Delete</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<input
|
||||
value={item.title}
|
||||
onChange={(e) => updateItem(i, 'title', e.target.value)}
|
||||
placeholder="Title"
|
||||
className="px-3 py-2 border border-gray-200 rounded text-sm"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
value={item.image}
|
||||
onChange={(e) => updateItem(i, 'image', e.target.value)}
|
||||
placeholder="Image path"
|
||||
className="flex-1 px-3 py-2 border border-gray-200 rounded text-sm"
|
||||
/>
|
||||
<ImageUpload onUploaded={(url) => updateItem(i, 'image', url)} />
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
value={item.description}
|
||||
onChange={(e) => updateItem(i, 'description', e.target.value)}
|
||||
placeholder="Description"
|
||||
rows={2}
|
||||
className="w-full mt-3 px-3 py-2 border border-gray-200 rounded text-sm resize-none"
|
||||
/>
|
||||
<input
|
||||
value={item.objectPosition || ''}
|
||||
onChange={(e) => updateItem(i, 'objectPosition', e.target.value)}
|
||||
placeholder="Object position (optional, e.g. center top)"
|
||||
className="w-full mt-2 px-3 py-2 border border-gray-200 rounded text-sm"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Main CMS Page ───────────────────────────────────────────────────────────
|
||||
|
||||
export default function UpdatePage() {
|
||||
const [authenticated, setAuthenticated] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<Tab>('artworks');
|
||||
|
||||
// Check if already authenticated from sessionStorage
|
||||
useEffect(() => {
|
||||
const pw = sessionStorage.getItem('admin_password');
|
||||
if (pw) {
|
||||
fetch('/api/cms/auth', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password: pw }),
|
||||
}).then((res) => {
|
||||
if (res.ok) setAuthenticated(true);
|
||||
else sessionStorage.removeItem('admin_password');
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!authenticated) {
|
||||
return <AuthGate onAuth={() => setAuthenticated(true)} />;
|
||||
}
|
||||
|
||||
const tabs: { key: Tab; label: string }[] = [
|
||||
{ key: 'artworks', label: 'Artworks' },
|
||||
{ key: 'services', label: 'Services' },
|
||||
{ key: 'testimonials', label: 'Testimonials' },
|
||||
{ key: 'events', label: 'Events' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--bg-cream)] pt-24 pb-12 px-4">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h1 className="text-3xl font-light">Content Manager</h1>
|
||||
<button
|
||||
onClick={() => {
|
||||
sessionStorage.removeItem('admin_password');
|
||||
setAuthenticated(false);
|
||||
}}
|
||||
className="text-sm text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-2 mb-8 border-b border-gray-200 pb-2">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={`px-4 py-2 rounded-t-lg text-sm transition-colors ${
|
||||
activeTab === tab.key
|
||||
? 'bg-white border border-b-0 border-gray-200 text-[var(--text-dark)]'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'artworks' && <ArtworksTab />}
|
||||
{activeTab === 'services' && <ServicesTab />}
|
||||
{activeTab === 'testimonials' && <TestimonialsTab />}
|
||||
{activeTab === 'events' && <EventsTab />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import Link from 'next/link';
|
||||
|
||||
export default 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="/about" className="footer-link">About</Link>
|
||||
<Link href="/services" className="footer-link">Services</Link>
|
||||
<Link href="/re-evolution-art" 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>
|
||||
<a href="https://soundcloud.com/xhiva" target="_blank" rel="noopener noreferrer" className="footer-link">
|
||||
SoundCloud
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import type { Artwork } from '@/lib/data/artworks';
|
||||
|
||||
interface LightboxGalleryProps {
|
||||
artworks: Artwork[];
|
||||
}
|
||||
|
||||
export default function LightboxGallery({ artworks }: LightboxGalleryProps) {
|
||||
const [selectedArtwork, setSelectedArtwork] = useState<Artwork | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
const navLinks = [
|
||||
{ href: '/', label: 'Home' },
|
||||
{ href: '/art', label: 'Art' },
|
||||
{ href: '/about', label: 'About' },
|
||||
{ href: '/services', label: 'Services' },
|
||||
{
|
||||
href: '/re-evolution-art',
|
||||
label: 'Re Evolution Art',
|
||||
children: [
|
||||
{ href: '/our-artists', label: 'Our Artists' },
|
||||
{ href: '/our-ambassadors', label: 'Our Ambassadors' },
|
||||
{ href: '/our-artisans', label: 'Our Artisans' },
|
||||
],
|
||||
},
|
||||
{ href: '/contact', label: 'Contact' },
|
||||
];
|
||||
|
||||
export default function Navigation() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const [mobileSubMenuOpen, setMobileSubMenuOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
|
||||
const isActive = (href: string) => {
|
||||
if (href === '/') return pathname === '/';
|
||||
return pathname.startsWith(href);
|
||||
};
|
||||
|
||||
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">
|
||||
{navLinks.map((link) =>
|
||||
link.children ? (
|
||||
<div key={link.href} className="nav-dropdown-wrapper">
|
||||
<Link
|
||||
href={link.href}
|
||||
className={`nav-link ${isActive(link.href) || link.children.some(c => isActive(c.href)) ? 'nav-link-active' : ''}`}
|
||||
>
|
||||
{link.label}
|
||||
<svg className="nav-chevron" width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M2 4l3 3 3-3" />
|
||||
</svg>
|
||||
</Link>
|
||||
<div className="nav-dropdown">
|
||||
{link.children.map((child) => (
|
||||
<Link
|
||||
key={child.href}
|
||||
href={child.href}
|
||||
className={`nav-dropdown-item ${isActive(child.href) ? 'nav-link-active' : ''}`}
|
||||
>
|
||||
{child.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={`nav-link ${isActive(link.href) ? 'nav-link-active' : ''}`}
|
||||
>
|
||||
{link.label}
|
||||
</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); setMobileSubMenuOpen(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>
|
||||
|
||||
{navLinks.map((link) =>
|
||||
link.children ? (
|
||||
<div key={link.href} className="flex flex-col items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
href={link.href}
|
||||
className={`nav-link text-xl ${isActive(link.href) ? 'nav-link-active' : ''}`}
|
||||
onClick={() => { setMobileMenuOpen(false); setMobileSubMenuOpen(false); }}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => setMobileSubMenuOpen(!mobileSubMenuOpen)}
|
||||
className="p-1"
|
||||
aria-label="Toggle submenu"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 10 10"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
className={`transition-transform duration-200 ${mobileSubMenuOpen ? 'rotate-180' : ''}`}
|
||||
>
|
||||
<path d="M2 4l3 3 3-3" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{mobileSubMenuOpen && (
|
||||
<div className="flex flex-col items-center gap-3 mt-3">
|
||||
{link.children.map((child) => (
|
||||
<Link
|
||||
key={child.href}
|
||||
href={child.href}
|
||||
className={`nav-link text-base opacity-70 ${isActive(child.href) ? 'nav-link-active' : ''}`}
|
||||
onClick={() => { setMobileMenuOpen(false); setMobileSubMenuOpen(false); }}
|
||||
>
|
||||
{child.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={`nav-link text-xl ${isActive(link.href) ? 'nav-link-active' : ''}`}
|
||||
onClick={() => { setMobileMenuOpen(false); setMobileSubMenuOpen(false); }}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { readFileSync, existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const DATA_DIR = process.env.CMS_DATA_DIR || '/app/data/content';
|
||||
|
||||
export function readData<T>(resource: string, fallback: T[]): T[] {
|
||||
const filePath = path.join(DATA_DIR, `${resource}.json`);
|
||||
try {
|
||||
if (existsSync(filePath)) {
|
||||
const raw = readFileSync(filePath, 'utf-8');
|
||||
return JSON.parse(raw) as T[];
|
||||
}
|
||||
} catch {
|
||||
// JSON parse error or read error — fall back to defaults
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function writeData<T>(resource: string, data: T[]): void {
|
||||
const { writeFileSync, mkdirSync } = require('fs') as typeof import('fs');
|
||||
mkdirSync(DATA_DIR, { recursive: true });
|
||||
const filePath = path.join(DATA_DIR, `${resource}.json`);
|
||||
writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
export function validateAuth(request: Request): boolean {
|
||||
const password = request.headers.get('X-Admin-Password');
|
||||
const expected = process.env.ADMIN_PASSWORD;
|
||||
if (!expected) return false;
|
||||
return password === expected;
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
export interface Artwork {
|
||||
src: string;
|
||||
title: string;
|
||||
price?: string;
|
||||
available?: boolean;
|
||||
medium?: string;
|
||||
dimensions?: string;
|
||||
}
|
||||
|
||||
export const featuredArtworks: Artwork[] = [
|
||||
{
|
||||
src: '/images/art/rainbow-tara.webp',
|
||||
title: 'Rainbow Tara',
|
||||
price: 'CHF 4,800',
|
||||
available: true,
|
||||
medium: 'Acrylic on canvas',
|
||||
},
|
||||
{
|
||||
src: '/images/art/amazonas.webp',
|
||||
title: 'Amazonas',
|
||||
price: 'CHF 550',
|
||||
available: true,
|
||||
medium: 'Mixed media on canvas',
|
||||
},
|
||||
{
|
||||
src: '/images/art/mujer-jaguar.webp',
|
||||
title: 'Mujer Jaguar',
|
||||
price: 'CHF 880',
|
||||
available: true,
|
||||
medium: 'Acrylic on canvas',
|
||||
},
|
||||
{
|
||||
src: '/images/art/coming-from-the-darkness.webp',
|
||||
title: 'From the Dark',
|
||||
price: 'CHF 1,650',
|
||||
available: true,
|
||||
medium: 'Acrylic on canvas',
|
||||
},
|
||||
{
|
||||
src: '/images/art/la-illimani.webp',
|
||||
title: 'Alax Pacha',
|
||||
price: 'CHF 6,500',
|
||||
available: true,
|
||||
medium: 'Acrylic on canvas',
|
||||
},
|
||||
{
|
||||
src: '/images/art/white-tara.webp',
|
||||
title: 'Kurukulla',
|
||||
price: 'CHF 1,450',
|
||||
available: true,
|
||||
medium: 'Acrylic on canvas',
|
||||
},
|
||||
];
|
||||
|
||||
export const allArtworks: Artwork[] = [
|
||||
{ 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 Evolucion 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: 'Sabiduria Ancestral' },
|
||||
{ src: '/images/art/amor-astral.webp', title: 'Amor Astral' },
|
||||
{ src: '/images/art/escencia-mistica.webp', title: 'Escencia Mistica' },
|
||||
{ src: '/images/art/raiz.webp', title: 'Raiz' },
|
||||
{ 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' },
|
||||
];
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
export interface ReEvolutionEvent {
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
objectPosition?: string;
|
||||
}
|
||||
|
||||
export const events: ReEvolutionEvent[] = [
|
||||
{
|
||||
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 Zurich',
|
||||
description: 'Curated week featuring international artists, performances, live music, talks and workshops.',
|
||||
image: '/images/reevolution/event-3.jpg',
|
||||
},
|
||||
{
|
||||
title: 'PULSAR',
|
||||
description: 'Electronic music experiences bridging conscious dance, DJ sets and expanded states of presence.',
|
||||
image: '/images/reevolution/dj-xhiva.jpg',
|
||||
objectPosition: 'center top',
|
||||
},
|
||||
{
|
||||
title: 'Collaborative Projects',
|
||||
description: 'Cross-cultural creative partnerships connecting artists, ritualists and independent creators across borders.',
|
||||
image: '/images/reevolution/event-5.jpg',
|
||||
},
|
||||
];
|
||||
|
||||
export const joinRoles = [
|
||||
{
|
||||
title: 'Artist',
|
||||
description: 'Exhibit your work, participate in collective shows and connect with a visionary community.',
|
||||
},
|
||||
{
|
||||
title: 'Artisan / Vendor',
|
||||
description: 'Offer your handcrafted goods, sacred objects or healing tools at our events and markets.',
|
||||
},
|
||||
{
|
||||
title: 'Facilitator',
|
||||
description: 'Lead workshops, ceremonies or movement sessions within our curated event spaces.',
|
||||
},
|
||||
{
|
||||
title: 'Ambassador',
|
||||
description: 'Represent Re Evolution Art in your region and help bridge cultures through creative dialogue.',
|
||||
},
|
||||
{
|
||||
title: 'Partner / Sponsor',
|
||||
description: 'Support our mission and gain visibility within a conscious, culturally engaged community.',
|
||||
},
|
||||
{
|
||||
title: 'Member',
|
||||
description: 'Join our community for early access to events, workshops and collaborative opportunities.',
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
export interface Service {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
duration: string;
|
||||
price?: string;
|
||||
description: string;
|
||||
longDescription?: string;
|
||||
color: string;
|
||||
image: string;
|
||||
highlighted?: boolean;
|
||||
recommendedFor?: string[];
|
||||
}
|
||||
|
||||
export const services: Service[] = [
|
||||
{
|
||||
title: 'Crystal Therapy',
|
||||
subtitle: 'Nervous System Alignment',
|
||||
duration: 'Crystal reading / 1 hour',
|
||||
price: '150 CHF',
|
||||
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.',
|
||||
longDescription: 'Working with crystal grids, elemental alignment and somatic attunement, these sessions create a deeply held space for the nervous system to regulate and integrate. Through intuitive mapping of energetic patterns, we address the emotional, physical, mental and spiritual bodies — supporting coherence, clarity and embodied presence.',
|
||||
color: 'lavender',
|
||||
image: '/images/services/crystal-therapy.webp',
|
||||
recommendedFor: [
|
||||
'Nervous system dysregulation or chronic stress',
|
||||
'Emotional processing and integration',
|
||||
'Post-ceremony or post-retreat integration',
|
||||
'Energetic clearing and realignment',
|
||||
'Those seeking clarity and inner coherence',
|
||||
],
|
||||
},
|
||||
{
|
||||
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.',
|
||||
longDescription: 'The Temazcal (sweat lodge) is one of the oldest ceremonial practices in the Americas. Guided by Ximena for over 15 years, these ceremonies work with fire, earth, water and air to support deep purification, emotional release, closure and renewal. The contained ritual space holds all four bodies — physical, emotional, mental and spiritual — in a process of death and rebirth.',
|
||||
color: 'mint',
|
||||
image: '/images/services/temazcal.jpg',
|
||||
recommendedFor: [
|
||||
'Deep purification and energetic cleansing',
|
||||
'Life transitions, closure and new beginnings',
|
||||
'Reconnection with the body and elements',
|
||||
'Group ceremonial experience',
|
||||
'Those drawn to ancestral healing traditions',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Premium Transformational Session',
|
||||
subtitle: 'Deep Integration',
|
||||
duration: '2 Hours',
|
||||
price: '220 CHF',
|
||||
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.',
|
||||
longDescription: 'A deeply personalised 2-hour session designed for those navigating significant life transitions, post-ceremonial integration or emotional transformation. Combining somatic bodywork, crystal energetic mapping, astrological guidance and ritual elements, this premium container supports the full embodiment of insight and conscious transformation.',
|
||||
color: 'rose',
|
||||
image: '/images/services/deep-integration.webp',
|
||||
recommendedFor: [
|
||||
'Life transitions and major decisions',
|
||||
'Post-ceremony or plant medicine integration',
|
||||
'Deep emotional transformation',
|
||||
'Those seeking a comprehensive, tailored session',
|
||||
'Integration of spiritual experiences into daily life',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Soul Portrait',
|
||||
subtitle: 'Art Alchemy',
|
||||
duration: '3-4 sessions',
|
||||
price: 'CHF 1,200 - 2,500+',
|
||||
description: 'Weaving visionary art with therapeutic process through guided creative immersion. A deeply personal co-creation — by application only — where your inner landscape becomes a living artwork.',
|
||||
longDescription: 'The Soul Portrait is a deeply intimate co-creative process, available by application only. Over 3-4 sessions, Ximena channels the essence of your inner landscape into a unique visionary artwork. This is not a commission in the traditional sense — it is a therapeutic and artistic journey, weaving intuitive vision, ancestral memory and energetic mapping into a living portrait of your soul.',
|
||||
color: 'pink',
|
||||
image: '/images/art/soul-agreement.webp',
|
||||
highlighted: true,
|
||||
recommendedFor: [
|
||||
'Those seeking a deeply personal artwork',
|
||||
'Individuals on a transformational path',
|
||||
'Collectors of visionary and sacred art',
|
||||
'Those who wish to see their inner world reflected in art',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const methodologySteps = [
|
||||
{ step: 1, title: 'Initial Contact', description: 'Reach out through the contact form or booking system to share your intention.' },
|
||||
{ step: 2, title: 'Discovery Call', description: 'A brief conversation to understand your needs and match you with the right session.' },
|
||||
{ step: 3, title: 'Preparation', description: 'Guidance on how to prepare — physically, emotionally and energetically.' },
|
||||
{ step: 4, title: 'Sacred Space', description: 'The session space is prepared with intention, crystals, elements and ritual objects.' },
|
||||
{ step: 5, title: 'Opening', description: 'We open the container together through breath, presence and intention setting.' },
|
||||
{ step: 6, title: 'The Work', description: 'The core session — somatic, energetic, creative or ceremonial — guided by your needs.' },
|
||||
{ step: 7, title: 'Integration', description: 'Time to process, share insights and receive guidance for continued integration.' },
|
||||
{ step: 8, title: 'Follow-Up', description: 'Post-session support and recommendations for your ongoing journey.' },
|
||||
];
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
export interface Testimonial {
|
||||
quote: string;
|
||||
author: string;
|
||||
context?: string;
|
||||
}
|
||||
|
||||
export const testimonials: Testimonial[] = [
|
||||
{
|
||||
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',
|
||||
context: 'Crystal Therapy',
|
||||
},
|
||||
{
|
||||
quote: 'The sweat lodge ceremony was powerfully moving, highlighting the integration of healing, compassion and authentic expression.',
|
||||
author: 'Kermit Goodman',
|
||||
context: 'Temazcal',
|
||||
},
|
||||
{
|
||||
quote: 'The experience was powerfully transformative — it changed my understanding of ceremony itself.',
|
||||
author: 'Verana Bailowitz',
|
||||
context: 'Ceremonial Work',
|
||||
},
|
||||
{
|
||||
quote: 'The crystal reading opened channels I didn\'t know existed. Nadine\'s presence held a space where my body could finally release what it had been carrying.',
|
||||
author: 'Nadine',
|
||||
context: 'Crystal Reading',
|
||||
},
|
||||
{
|
||||
quote: 'Her artistry carries the weight of ancestral memory. Working with Ximena is like being witnessed by something ancient and deeply compassionate.',
|
||||
author: 'Miguel Kavlin',
|
||||
context: 'Soul Portrait',
|
||||
},
|
||||
{
|
||||
quote: 'In her presence the healing is happening. There is no force, no agenda — just a deep, quiet power that moves through everything she touches.',
|
||||
author: 'Alisson Cote',
|
||||
context: 'Transformational Session',
|
||||
},
|
||||
];
|
||||
Loading…
Reference in New Issue