ci: use internal registry (bypass Cloudflare upload limit)
CI/CD / deploy (push) Failing after 41s
Details
CI/CD / deploy (push) Failing after 41s
Details
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7e3ed3da6c
commit
c7b97aa0cc
|
|
@ -9,8 +9,8 @@ on:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: gitea.jeffemmett.com
|
REGISTRY: localhost:3000
|
||||||
IMAGE: gitea.jeffemmett.com/jeffemmett/xhivart-mirror
|
IMAGE: localhost:3000/jeffemmett/xhivart-mirror
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,10 @@ RUN mkdir -p /data && chown nextjs:nodejs /data
|
||||||
RUN mkdir .next
|
RUN mkdir .next
|
||||||
RUN chown nextjs:nodejs .next
|
RUN chown nextjs:nodejs .next
|
||||||
|
|
||||||
|
# Create CMS data directories
|
||||||
|
RUN mkdir -p /app/data/content /app/data/uploads
|
||||||
|
RUN chown -R nextjs:nodejs /app/data
|
||||||
|
|
||||||
# Copy standalone output
|
# Copy standalone output
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ services:
|
||||||
- SMTP_FROM=${SMTP_FROM}
|
- SMTP_FROM=${SMTP_FROM}
|
||||||
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
||||||
volumes:
|
volumes:
|
||||||
- xhivart-data:/data
|
- xhivart-data:/app/data
|
||||||
networks:
|
networks:
|
||||||
- traefik-public
|
- traefik-public
|
||||||
labels:
|
labels:
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ function escapeHtml(str: string): string {
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { name, email, message } = body;
|
const { name, email, service, message } = body;
|
||||||
|
|
||||||
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
||||||
return NextResponse.json({ error: 'Name is required' }, { status: 400 });
|
return NextResponse.json({ error: 'Name is required' }, { status: 400 });
|
||||||
|
|
@ -36,8 +36,16 @@ export async function POST(request: Request) {
|
||||||
|
|
||||||
const safeName = escapeHtml(name.trim());
|
const safeName = escapeHtml(name.trim());
|
||||||
const safeEmail = escapeHtml(email.trim());
|
const safeEmail = escapeHtml(email.trim());
|
||||||
|
const safeService = service && typeof service === 'string' ? escapeHtml(service.trim()) : '';
|
||||||
const safeMessage = escapeHtml(message.trim());
|
const safeMessage = escapeHtml(message.trim());
|
||||||
|
|
||||||
|
const serviceSection = safeService
|
||||||
|
? `<div style="margin-bottom: 24px;">
|
||||||
|
<p style="font-family: Montserrat, sans-serif; font-size: 11px; letter-spacing: 0.15em; text-transform: uppercase; color: #6b6b6b; margin: 0 0 4px 0;">SERVICE</p>
|
||||||
|
<p style="font-size: 16px; color: #2d2d2d; margin: 0;">${safeService}</p>
|
||||||
|
</div>`
|
||||||
|
: '';
|
||||||
|
|
||||||
await transporter.sendMail({
|
await transporter.sendMail({
|
||||||
from: `XHIVA Art <${process.env.SMTP_FROM || process.env.SMTP_USER}>`,
|
from: `XHIVA Art <${process.env.SMTP_FROM || process.env.SMTP_USER}>`,
|
||||||
to: 'xhivart@gmail.com',
|
to: 'xhivart@gmail.com',
|
||||||
|
|
@ -60,13 +68,14 @@ export async function POST(request: Request) {
|
||||||
<a href="mailto:${safeEmail}" style="color: #c9a962;">${safeEmail}</a>
|
<a href="mailto:${safeEmail}" style="color: #c9a962;">${safeEmail}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
${serviceSection}
|
||||||
<div style="margin-bottom: 24px;">
|
<div style="margin-bottom: 24px;">
|
||||||
<p style="font-family: Montserrat, sans-serif; font-size: 11px; letter-spacing: 0.15em; text-transform: uppercase; color: #6b6b6b; margin: 0 0 4px 0;">MESSAGE</p>
|
<p style="font-family: Montserrat, sans-serif; font-size: 11px; letter-spacing: 0.15em; text-transform: uppercase; color: #6b6b6b; margin: 0 0 4px 0;">MESSAGE</p>
|
||||||
<p style="font-size: 16px; color: #2d2d2d; margin: 0; white-space: pre-wrap;">${safeMessage}</p>
|
<p style="font-size: 16px; color: #2d2d2d; margin: 0; white-space: pre-wrap;">${safeMessage}</p>
|
||||||
</div>
|
</div>
|
||||||
<div style="border-top: 1px solid #e5e5e5; padding-top: 20px; margin-top: 30px;">
|
<div style="border-top: 1px solid #e5e5e5; padding-top: 20px; margin-top: 30px;">
|
||||||
<p style="font-family: Montserrat, sans-serif; font-size: 10px; letter-spacing: 0.1em; color: #6b6b6b; margin: 0;">
|
<p style="font-family: Montserrat, sans-serif; font-size: 10px; letter-spacing: 0.1em; color: #6b6b6b; margin: 0;">
|
||||||
Sent from xhivart.jeffemmett.com contact form
|
Sent from xhiva.art contact form
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -89,12 +89,71 @@
|
||||||
color: var(--text-dark);
|
color: var(--text-dark);
|
||||||
transition: color 0.3s ease;
|
transition: color 0.3s ease;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link:hover {
|
.nav-link:hover {
|
||||||
color: var(--accent-gold);
|
color: var(--accent-gold);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-link-active {
|
||||||
|
color: var(--accent-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-chevron {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown wrapper */
|
||||||
|
.nav-dropdown-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
min-width: 200px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: opacity 0.2s ease, visibility 0.2s ease;
|
||||||
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-dropdown-wrapper:hover .nav-dropdown {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-dropdown-wrapper:hover .nav-chevron {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-dropdown-item {
|
||||||
|
display: block;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
font-family: var(--font-montserrat), 'Montserrat', sans-serif;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text-dark);
|
||||||
|
transition: color 0.2s ease, background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-dropdown-item:hover {
|
||||||
|
color: var(--accent-gold);
|
||||||
|
background: rgba(201, 169, 98, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
/* Elegant button styles */
|
/* Elegant button styles */
|
||||||
.btn-outline {
|
.btn-outline {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
@ -178,6 +237,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Page hero — shorter than homepage hero */
|
||||||
|
.page-hero {
|
||||||
|
min-height: 60vh;
|
||||||
|
padding: 8rem 2rem 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.page-hero {
|
||||||
|
padding: 10rem 4rem 6rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Decorative elements */
|
/* Decorative elements */
|
||||||
.divider {
|
.divider {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
|
|
@ -219,6 +290,41 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Price tag */
|
||||||
|
.price-tag {
|
||||||
|
font-family: var(--font-montserrat), 'Montserrat', sans-serif;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: var(--accent-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Methodology step */
|
||||||
|
.methodology-step {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-left: 2px solid rgba(201, 169, 98, 0.3);
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.methodology-step:hover {
|
||||||
|
border-color: var(--accent-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Role card */
|
||||||
|
.role-card {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 2rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-card:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-color: var(--accent-gold);
|
||||||
|
}
|
||||||
|
|
||||||
/* Footer */
|
/* Footer */
|
||||||
.footer-link {
|
.footer-link {
|
||||||
font-family: var(--font-montserrat), 'Montserrat', sans-serif;
|
font-family: var(--font-montserrat), 'Montserrat', sans-serif;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Cormorant_Garamond, Montserrat } from "next/font/google";
|
import { Cormorant_Garamond, Montserrat } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import Navigation from "@/components/Navigation";
|
||||||
|
import Footer from "@/components/Footer";
|
||||||
|
|
||||||
const cormorant = Cormorant_Garamond({
|
const cormorant = Cormorant_Garamond({
|
||||||
variable: "--font-cormorant",
|
variable: "--font-cormorant",
|
||||||
|
|
@ -34,7 +36,9 @@ export default function RootLayout({
|
||||||
<body
|
<body
|
||||||
className={`${cormorant.variable} ${montserrat.variable} antialiased`}
|
className={`${cormorant.variable} ${montserrat.variable} antialiased`}
|
||||||
>
|
>
|
||||||
{children}
|
<Navigation />
|
||||||
|
<main>{children}</main>
|
||||||
|
<Footer />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
410
src/app/page.tsx
410
src/app/page.tsx
|
|
@ -1,12 +1,408 @@
|
||||||
import { getArtworks, getEvents, getServices } from '@/lib/data';
|
import Link from 'next/link';
|
||||||
import HomeClient from './home-client';
|
import Image from 'next/image';
|
||||||
|
import { services as defaultServices } from '@/lib/data/services';
|
||||||
|
import { testimonials as defaultTestimonials } from '@/lib/data/testimonials';
|
||||||
|
import { readData } from '@/lib/cms';
|
||||||
|
import type { Service } from '@/lib/data/services';
|
||||||
|
import type { Testimonial } from '@/lib/data/testimonials';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export default function Home() {
|
// Hero Section
|
||||||
const artworks = getArtworks();
|
function HeroSection() {
|
||||||
const events = getEvents();
|
return (
|
||||||
const services = getServices();
|
<section className="min-h-screen relative flex flex-col items-center justify-center text-center px-6 pt-20">
|
||||||
|
<div className="absolute inset-0 z-0">
|
||||||
|
<Image
|
||||||
|
src="/images/hero/hero-main.jpg"
|
||||||
|
alt="Ximena Xaguar"
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-b from-[var(--bg-cream)]/90 via-[var(--bg-cream)]/70 to-[var(--bg-cream)]/90" />
|
||||||
|
</div>
|
||||||
|
|
||||||
return <HomeClient artworks={artworks} events={events} services={services} />;
|
<div className="max-w-4xl mx-auto relative z-10">
|
||||||
|
<p className="font-sans-alt text-xs tracking-[0.3em] text-[var(--text-muted)] mb-6 fade-in">
|
||||||
|
MULTIDISCIPLINARY VISIONARY ARTIST
|
||||||
|
</p>
|
||||||
|
<h1 className="text-5xl md:text-7xl lg:text-8xl font-light tracking-wider mb-8 fade-in stagger-1">
|
||||||
|
XIMENA XAGUAR
|
||||||
|
</h1>
|
||||||
|
<div className="divider fade-in stagger-2"></div>
|
||||||
|
<p className="text-lg md:text-xl max-w-2xl mx-auto mb-16 leading-relaxed opacity-80 fade-in stagger-2">
|
||||||
|
Working at the intersection of art, ritual and embodied presence, creating experiences
|
||||||
|
that support clarity, integration, exploration and transformation.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center fade-in stagger-3">
|
||||||
|
<Link href="/art" className="btn-outline">
|
||||||
|
Explore Art & Ritual Sessions
|
||||||
|
</Link>
|
||||||
|
<Link href="/re-evolution-art" className="btn-filled">
|
||||||
|
Discover Re Evolution Art
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 animate-bounce opacity-50 z-10">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||||
|
<path d="M12 5v14M5 12l7 7 7-7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ritual Art Alchemy Preview
|
||||||
|
function RitualArtSection() {
|
||||||
|
return (
|
||||||
|
<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/art/featured.jpg?v=2"
|
||||||
|
alt="Visionary Art by Ximena Xaguar"
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||||
|
VISUAL ALCHEMY
|
||||||
|
</p>
|
||||||
|
<h2 className="text-4xl md:text-5xl font-light mb-6">
|
||||||
|
Ritual Art Alchemy
|
||||||
|
</h2>
|
||||||
|
<div className="divider"></div>
|
||||||
|
<p className="text-lg leading-relaxed mb-6 opacity-80">
|
||||||
|
My art is my ritual — a journey of transformation. Each painting emerges from
|
||||||
|
cycles of death and rebirth, shadow work, intuitive vision and ancestral memory.
|
||||||
|
</p>
|
||||||
|
<p className="text-lg leading-relaxed mb-12 opacity-80">
|
||||||
|
Through symbolic language and universal cosmovision, I translate inner processes
|
||||||
|
into living images. These artworks are not objects. They are portals.
|
||||||
|
</p>
|
||||||
|
<div className="pt-2">
|
||||||
|
<Link href="/art" className="btn-outline">
|
||||||
|
Enter the Gallery
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Services Preview (abbreviated)
|
||||||
|
function ServicesPreview({ services }: { services: Service[] }) {
|
||||||
|
const previewServices = services.slice(0, 4);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
OFFERINGS
|
||||||
|
</p>
|
||||||
|
<h2 className="text-4xl md:text-5xl font-light mb-6">
|
||||||
|
Ritual Healing & Integration
|
||||||
|
</h2>
|
||||||
|
<div className="divider"></div>
|
||||||
|
<p className="text-lg max-w-2xl mx-auto opacity-80">
|
||||||
|
Decades of embodied practice since 2009. Current focus is integration and embodiment —
|
||||||
|
supporting people to anchor insight, develop inner coherence and regulate nervous systems
|
||||||
|
within deep transformation processes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-8">
|
||||||
|
{previewServices.map((service, index) => (
|
||||||
|
<div key={index} className={`group ${service.highlighted ? 'ring-2 ring-[var(--accent-gold)] rounded-xl' : ''}`}>
|
||||||
|
<div className="relative aspect-[4/3] rounded-t-xl overflow-hidden">
|
||||||
|
<Image
|
||||||
|
src={service.image}
|
||||||
|
alt={service.title}
|
||||||
|
fill
|
||||||
|
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={`service-card ${service.color} rounded-t-none`}>
|
||||||
|
<h3 className="text-2xl font-light mb-2">{service.title}</h3>
|
||||||
|
<p className="font-sans-alt text-xs tracking-widest text-[var(--text-muted)] mb-1">
|
||||||
|
{service.subtitle}
|
||||||
|
</p>
|
||||||
|
<p className="font-sans-alt text-xs tracking-widest text-[var(--accent-gold)] mb-6">
|
||||||
|
{service.duration}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm leading-relaxed opacity-80">
|
||||||
|
{service.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center mt-20">
|
||||||
|
<Link href="/services" className="btn-outline">
|
||||||
|
View All Services
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re Evolution Art Preview
|
||||||
|
function ReEvolutionPreview() {
|
||||||
|
return (
|
||||||
|
<section className="section dark-section">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<div className="relative w-full max-w-96 aspect-[3/2] mx-auto mb-2 overflow-hidden">
|
||||||
|
<Image
|
||||||
|
src="/images/reevolution/logo.png"
|
||||||
|
alt="Re Evolution Art Logo"
|
||||||
|
fill
|
||||||
|
className="object-contain object-center scale-150"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||||
|
CULTURAL PLATFORM
|
||||||
|
</p>
|
||||||
|
<h2 style={{ fontFamily: "'Krown', sans-serif", fontWeight: 700 }} className="text-4xl md:text-5xl mb-6">
|
||||||
|
Re Evolution Art
|
||||||
|
</h2>
|
||||||
|
<div className="divider"></div>
|
||||||
|
<p style={{ fontFamily: "'Narrenschiff', sans-serif" }} className="text-lg max-w-3xl mx-auto opacity-80">
|
||||||
|
A visionary cultural platform based in Switzerland and rooted in Bolivia. Through exhibitions,
|
||||||
|
immersive gatherings, TRIBAL events and collaborative projects, we bring together artists,
|
||||||
|
ritualists, musicians and independent creators engaged in inner work and cultural dialogue.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center gap-4 mt-8">
|
||||||
|
<Link href="/re-evolution-art" className="btn-outline btn-outline-light" style={{ fontFamily: "'Narrenschiff', sans-serif" }}>
|
||||||
|
Discover the Platform
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Testimonials (showing 3 on homepage)
|
||||||
|
function TestimonialsSection({ testimonials }: { testimonials: Testimonial[] }) {
|
||||||
|
const homeTestimonials = testimonials.slice(0, 3);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="section bg-white">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||||
|
TESTIMONIALS
|
||||||
|
</p>
|
||||||
|
<h2 className="text-4xl md:text-5xl font-light mb-6">
|
||||||
|
Words of Gratitude
|
||||||
|
</h2>
|
||||||
|
<div className="divider"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-8">
|
||||||
|
{homeTestimonials.map((testimonial, index) => (
|
||||||
|
<div key={index} className="testimonial-card">
|
||||||
|
<span className="quote-mark">“</span>
|
||||||
|
<p className="testimonial mb-6">
|
||||||
|
{testimonial.quote}
|
||||||
|
</p>
|
||||||
|
<p className="font-sans-alt text-xs tracking-widest text-[var(--accent-gold)]">
|
||||||
|
— {testimonial.author}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Work With Me
|
||||||
|
function WorkWithMeSection() {
|
||||||
|
return (
|
||||||
|
<section className="section relative overflow-hidden">
|
||||||
|
<div className="absolute inset-0 z-0 bg-[#0f0f0f]">
|
||||||
|
<Image
|
||||||
|
src="/images/art/wayra-bg.webp"
|
||||||
|
alt="Wayra"
|
||||||
|
fill
|
||||||
|
className="object-contain opacity-40"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-3xl mx-auto relative z-10">
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-4xl md:text-5xl font-light mb-6 text-[var(--text-light)]">
|
||||||
|
Work With Me
|
||||||
|
</h2>
|
||||||
|
<div className="divider"></div>
|
||||||
|
<p className="text-lg leading-relaxed mb-16 opacity-80 max-w-2xl mx-auto text-[var(--text-light)]">
|
||||||
|
This work is for those ready to meet themselves honestly with courage,
|
||||||
|
sensitivity and presence.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||||
|
<Link href="/services" className="btn-outline btn-outline-light">
|
||||||
|
Explore Sessions
|
||||||
|
</Link>
|
||||||
|
<Link href="/contact" className="btn-filled" style={{ background: 'var(--accent-gold)', borderColor: 'var(--accent-gold)' }}>
|
||||||
|
Contact Me
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Social Section
|
||||||
|
function SocialSection() {
|
||||||
|
const instagramPosts = [
|
||||||
|
{ image: '/images/art/soul-agreement.webp', alt: 'Soul Agreement' },
|
||||||
|
{ image: '/images/art/goddess.webp?v=2', alt: 'Goddess' },
|
||||||
|
{ image: '/images/art/twin-flames.webp', alt: 'Twin Flames' },
|
||||||
|
{ image: '/images/art/featured.jpg?v=2', alt: 'Featured Art' },
|
||||||
|
{ image: '/images/about/portrait-1.jpg', alt: 'Portrait' },
|
||||||
|
{ image: '/images/reevolution/dj-xhiva.jpg', alt: 'DJ XHIVA' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="section bg-[var(--bg-cream)]">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||||
|
FOLLOW THE JOURNEY
|
||||||
|
</p>
|
||||||
|
<h2 className="text-4xl md:text-5xl font-light mb-6">
|
||||||
|
Connect on Socials
|
||||||
|
</h2>
|
||||||
|
<div className="divider"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 mb-10 max-w-4xl mx-auto">
|
||||||
|
{instagramPosts.map((post, index) => (
|
||||||
|
<a
|
||||||
|
key={index}
|
||||||
|
href="https://www.instagram.com/xhiva_art"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="group bg-white rounded-xl shadow-md hover:shadow-xl transition-shadow duration-300 overflow-hidden border border-gray-100"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 px-3 py-2.5">
|
||||||
|
<div className="w-7 h-7 rounded-full bg-gradient-to-br from-[#f09433] via-[#e6683c] to-[#bc1888] p-[2px]">
|
||||||
|
<div className="w-full h-full rounded-full bg-white p-[1px]">
|
||||||
|
<div className="relative w-full h-full rounded-full overflow-hidden">
|
||||||
|
<Image src="/images/about/portrait-1.jpg" alt="xhiva_art" fill className="object-cover object-[30%_center]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-semibold text-[var(--text-dark)]">xhiva_art</span>
|
||||||
|
</div>
|
||||||
|
<div className="relative aspect-square">
|
||||||
|
<Image
|
||||||
|
src={post.image}
|
||||||
|
alt={post.alt}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
style={post.image.includes('portrait-1') ? { objectPosition: '30% center' } : post.image.includes('dj-xhiva') ? { objectPosition: 'center top' } : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="px-3 py-2.5 flex items-center gap-4">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[var(--text-dark)] group-hover:text-red-500 transition-colors">
|
||||||
|
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
|
||||||
|
</svg>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[var(--text-dark)]">
|
||||||
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
||||||
|
</svg>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[var(--text-dark)]">
|
||||||
|
<line x1="22" y1="2" x2="11" y2="13" /><polygon points="22 2 15 22 11 13 2 9 22 2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row justify-center items-center gap-4">
|
||||||
|
<a
|
||||||
|
href="https://www.instagram.com/xhiva_art"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="btn-filled"
|
||||||
|
>
|
||||||
|
Follow @xhiva_art on Instagram
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://www.facebook.com/XimenaXhivart"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="btn-outline"
|
||||||
|
>
|
||||||
|
Follow on Facebook
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Newsletter Section
|
||||||
|
function NewsletterSection() {
|
||||||
|
return (
|
||||||
|
<section className="section dark-section">
|
||||||
|
<div className="max-w-3xl mx-auto text-center">
|
||||||
|
<p className="font-sans-alt text-xs tracking-[0.2em] text-[var(--accent-gold)] mb-4">
|
||||||
|
STAY CONNECTED
|
||||||
|
</p>
|
||||||
|
<h2 className="text-4xl md:text-5xl font-light mb-6">
|
||||||
|
Be Part of the RE EVOLUTION
|
||||||
|
</h2>
|
||||||
|
<div className="divider"></div>
|
||||||
|
<p className="text-lg leading-relaxed mb-10 opacity-80 max-w-2xl mx-auto">
|
||||||
|
Receive updates on exhibitions, events, new artworks and offerings.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||||
|
<Link href="/contact" className="btn-outline btn-outline-light">
|
||||||
|
Get in Touch
|
||||||
|
</Link>
|
||||||
|
<a
|
||||||
|
href="https://www.instagram.com/xhiva_art"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="btn-outline btn-outline-light"
|
||||||
|
>
|
||||||
|
Follow on Instagram
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const services = readData<Service>('services', defaultServices);
|
||||||
|
const testimonials = readData<Testimonial>('testimonials', defaultTestimonials);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<HeroSection />
|
||||||
|
<RitualArtSection />
|
||||||
|
<ServicesPreview services={services} />
|
||||||
|
<ReEvolutionPreview />
|
||||||
|
<TestimonialsSection testimonials={testimonials} />
|
||||||
|
<WorkWithMeSection />
|
||||||
|
<SocialSection />
|
||||||
|
<NewsletterSection />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue