From 991059020bdfb8f5f66dacf3033404304e109f66 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Wed, 1 Apr 2026 12:23:04 -0700 Subject: [PATCH] Add untracked source files from site restructure 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 --- src/app/api/cms/artworks/route.ts | 24 + src/app/api/cms/auth/route.ts | 20 + src/app/api/cms/events/route.ts | 24 + src/app/api/cms/services/route.ts | 24 + src/app/api/cms/testimonials/route.ts | 21 + src/app/api/cms/upload/route.ts | 52 ++ src/app/art/page.tsx | 161 ++++++ src/app/contact/page.tsx | 278 ++++++++++ src/app/our-ambassadors/page.tsx | 77 +++ src/app/our-artisans/page.tsx | 78 +++ src/app/our-artists/page.tsx | 131 +++++ src/app/re-evolution-art/page.tsx | 230 +++++++++ src/app/services/page.tsx | 189 +++++++ src/app/update/page.tsx | 715 ++++++++++++++++++++++++++ src/components/Footer.tsx | 54 ++ src/components/Lightbox.tsx | 78 +++ src/components/Navigation.tsx | 168 ++++++ src/lib/cms.ts | 31 ++ src/lib/data/artworks.ts | 96 ++++ src/lib/data/events.ts | 62 +++ src/lib/data/services.ts | 93 ++++ src/lib/data/testimonials.ts | 38 ++ 22 files changed, 2644 insertions(+) create mode 100644 src/app/api/cms/artworks/route.ts create mode 100644 src/app/api/cms/auth/route.ts create mode 100644 src/app/api/cms/events/route.ts create mode 100644 src/app/api/cms/services/route.ts create mode 100644 src/app/api/cms/testimonials/route.ts create mode 100644 src/app/api/cms/upload/route.ts create mode 100644 src/app/art/page.tsx create mode 100644 src/app/contact/page.tsx create mode 100644 src/app/our-ambassadors/page.tsx create mode 100644 src/app/our-artisans/page.tsx create mode 100644 src/app/our-artists/page.tsx create mode 100644 src/app/re-evolution-art/page.tsx create mode 100644 src/app/services/page.tsx create mode 100644 src/app/update/page.tsx create mode 100644 src/components/Footer.tsx create mode 100644 src/components/Lightbox.tsx create mode 100644 src/components/Navigation.tsx create mode 100644 src/lib/cms.ts create mode 100644 src/lib/data/artworks.ts create mode 100644 src/lib/data/events.ts create mode 100644 src/lib/data/services.ts create mode 100644 src/lib/data/testimonials.ts 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'} + /> +
+
+ + +
+
+ +