diff --git a/src/app/api/cms/artworks/route.ts b/src/app/api/cms/artworks/route.ts new file mode 100644 index 0000000..fe87741 --- /dev/null +++ b/src/app/api/cms/artworks/route.ts @@ -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 }); + } +} diff --git a/src/app/api/cms/auth/route.ts b/src/app/api/cms/auth/route.ts new file mode 100644 index 0000000..9f3e3ef --- /dev/null +++ b/src/app/api/cms/auth/route.ts @@ -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 }); + } +} diff --git a/src/app/api/cms/events/route.ts b/src/app/api/cms/events/route.ts new file mode 100644 index 0000000..3e95f5e --- /dev/null +++ b/src/app/api/cms/events/route.ts @@ -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 }); + } +} diff --git a/src/app/api/cms/services/route.ts b/src/app/api/cms/services/route.ts new file mode 100644 index 0000000..cb003b7 --- /dev/null +++ b/src/app/api/cms/services/route.ts @@ -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 }); + } +} diff --git a/src/app/api/cms/testimonials/route.ts b/src/app/api/cms/testimonials/route.ts new file mode 100644 index 0000000..d5f32ff --- /dev/null +++ b/src/app/api/cms/testimonials/route.ts @@ -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 }); + } +} diff --git a/src/app/api/cms/upload/route.ts b/src/app/api/cms/upload/route.ts new file mode 100644 index 0000000..d86eec8 --- /dev/null +++ b/src/app/api/cms/upload/route.ts @@ -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 }); + } +} diff --git a/src/app/art/page.tsx b/src/app/art/page.tsx new file mode 100644 index 0000000..2c49324 --- /dev/null +++ b/src/app/art/page.tsx @@ -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('artworks-featured', defaultFeatured); + const allArtworks = readData('artworks', defaultAll); + + return ( + <> + {/* Page Hero */} +
+
+ Ritual Art Alchemy +
+
+
+

+ VISUAL ALCHEMY +

+

+ Ritual Art Alchemy +

+
+

+ 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. +

+
+
+ + {/* Artist Statement */} +
+
+

+ 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. +

+

+ 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. +

+
+
+ + {/* Featured Available Artworks */} +
+
+
+

+ AVAILABLE WORKS +

+

+ Featured Artworks +

+
+
+ +
+ {featuredArtworks.map((artwork, index) => ( +
+
+ {artwork.title} +
+
+

{artwork.title}

+ {artwork.medium && ( +

+ {artwork.medium} +

+ )} +

{artwork.price}

+ + Inquire → + +
+
+ ))} +
+
+
+ + {/* Biography Brief */} +
+
+
+
+ Ximena Xaguar +
+
+

+ THE ARTIST +

+

+ Ximena Xaguar +

+
+

+ Born in Bolivia, trained in the Visionary Realism tradition of Ernst Fuchs in Vienna. + Living and working between Switzerland and South America since 1996. +

+

+ Her paintings weave ancestral cosmovision with contemporary expression, carrying + the energy of ceremony, transformation and the sacred feminine. +

+ + Full Biography + +
+
+
+
+ + {/* Full Gallery */} +
+
+
+

+ GALLERY +

+

+ Visionary Artworks +

+
+
+ + +
+
+ + ); +} diff --git a/src/app/contact/page.tsx b/src/app/contact/page.tsx new file mode 100644 index 0000000..8bca075 --- /dev/null +++ b/src/app/contact/page.tsx @@ -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 */} +
+
+
+

+ GET IN TOUCH +

+

+ Begin Your Journey +

+
+

+ For inquiries about sessions, art commissions, or event collaborations, + please reach out through the form below or connect on social media. +

+
+
+ + {/* Contact Form + Sidebar */} +
+
+
+ {/* Form */} +
+ {status === 'success' ? ( +
+
+

Message Sent

+

+ Thank you for reaching out. I will get back to you soon. +

+ +
+ ) : ( +
+
+ + 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'} + /> +
+
+ + 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'} + /> +
+
+ + +
+
+ +